@telorun/kernel 0.78.0 → 0.80.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/dist/bundle/module-artifact.d.ts +15 -2
- package/dist/bundle/module-artifact.d.ts.map +1 -1
- package/dist/bundle/module-artifact.js +36 -13
- package/dist/bundle/module-artifact.js.map +1 -1
- package/dist/controller-loader.d.ts +62 -9
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +72 -64
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/bundle-loader.d.ts +2 -2
- package/dist/controller-loaders/bundle-loader.d.ts.map +1 -1
- package/dist/controller-loaders/bundle-loader.js +9 -3
- package/dist/controller-loaders/bundle-loader.js.map +1 -1
- package/dist/controller-loaders/napi-loader.d.ts +22 -1
- package/dist/controller-loaders/napi-loader.d.ts.map +1 -1
- package/dist/controller-loaders/napi-loader.js +35 -5
- package/dist/controller-loaders/napi-loader.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts +2 -1
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +81 -11
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controller-loaders/source-bundle-builder.d.ts +2 -1
- package/dist/controller-loaders/source-bundle-builder.d.ts.map +1 -1
- package/dist/controller-loaders/source-bundle-builder.js +4 -1
- package/dist/controller-loaders/source-bundle-builder.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +7 -3
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +12 -5
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/dependency-injection.d.ts.map +1 -1
- package/dist/dependency-injection.js +7 -7
- package/dist/dependency-injection.js.map +1 -1
- package/dist/evaluation-context.d.ts +33 -13
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +145 -4
- package/dist/evaluation-context.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/instance-declaration.d.ts +8 -0
- package/dist/instance-declaration.d.ts.map +1 -0
- package/dist/instance-declaration.js +34 -0
- package/dist/instance-declaration.js.map +1 -0
- package/dist/invocation-contract-binding.d.ts +15 -2
- package/dist/invocation-contract-binding.d.ts.map +1 -1
- package/dist/invocation-contract-binding.js +39 -4
- package/dist/invocation-contract-binding.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +78 -14
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-schemas.d.ts +1 -1
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +1 -1
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/manifest-sources/analysis-stamp.d.ts +12 -8
- package/dist/manifest-sources/analysis-stamp.d.ts.map +1 -1
- package/dist/manifest-sources/analysis-stamp.js +50 -23
- package/dist/manifest-sources/analysis-stamp.js.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.d.ts +45 -9
- package/dist/manifest-sources/local-manifest-cache-source.d.ts.map +1 -1
- package/dist/manifest-sources/local-manifest-cache-source.js +65 -12
- package/dist/manifest-sources/local-manifest-cache-source.js.map +1 -1
- package/dist/module-context.d.ts +5 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +13 -1
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +12 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +15 -0
- package/dist/resource-context.js.map +1 -1
- package/dist/runtime-seam.d.ts.map +1 -1
- package/dist/runtime-seam.js +9 -1
- package/dist/runtime-seam.js.map +1 -1
- package/dist/workspace-marker.d.ts +37 -0
- package/dist/workspace-marker.d.ts.map +1 -0
- package/dist/workspace-marker.js +68 -0
- package/dist/workspace-marker.js.map +1 -0
- package/dist/zone-context.d.ts +16 -1
- package/dist/zone-context.d.ts.map +1 -1
- package/dist/zone-context.js +61 -1
- package/dist/zone-context.js.map +1 -1
- package/package.json +3 -3
- package/src/bundle/module-artifact.ts +47 -15
- package/src/controller-loader.ts +115 -81
- package/src/controller-loaders/bundle-loader.ts +10 -2
- package/src/controller-loaders/napi-loader.ts +42 -3
- package/src/controller-loaders/npm-loader.ts +86 -8
- package/src/controller-loaders/source-bundle-builder.ts +6 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +7 -3
- package/src/controllers/resource-definition/resource-template-controller.ts +12 -5
- package/src/dependency-injection.ts +7 -7
- package/src/evaluation-context.ts +195 -15
- package/src/index.ts +7 -0
- package/src/instance-declaration.ts +36 -0
- package/src/invocation-contract-binding.ts +49 -1
- package/src/kernel.ts +95 -14
- package/src/manifest-schemas.ts +1 -0
- package/src/manifest-sources/analysis-stamp.ts +53 -25
- package/src/manifest-sources/local-manifest-cache-source.ts +68 -13
- package/src/module-context.ts +24 -3
- package/src/resource-context.ts +18 -0
- package/src/runtime-seam.ts +9 -1
- package/src/workspace-marker.ts +68 -0
- package/src/zone-context.ts +70 -1
package/src/kernel.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ContractValidatorFactory,
|
|
18
18
|
resolveBoundContract,
|
|
19
19
|
} from "./invocation-contract-binding.js";
|
|
20
|
+
import { readProjectionRef, type ProjectionScope } from "@telorun/analyzer";
|
|
20
21
|
import {
|
|
21
22
|
ControllerContext,
|
|
22
23
|
ControllerPolicy,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
ResourceDefinition,
|
|
27
28
|
ResourceInstance,
|
|
28
29
|
ResourceManifest,
|
|
30
|
+
stampRefIdentity,
|
|
29
31
|
RuntimeError,
|
|
30
32
|
RuntimeEvent,
|
|
31
33
|
type BootTarget,
|
|
@@ -49,6 +51,7 @@ import { ambientInvokeContext } from "./evaluation-context.js";
|
|
|
49
51
|
import { ModuleContext } from "./module-context.js";
|
|
50
52
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
51
53
|
import { mintResourceHandle } from "./resource-handle.js";
|
|
54
|
+
import { declarationOfInstance, recordInstanceDeclaration } from "./instance-declaration.js";
|
|
52
55
|
import { nodeHostVersions } from "./host-versions.js";
|
|
53
56
|
import { nodeCelHandlers } from "./cel-handlers.js";
|
|
54
57
|
import { parseRef, seedInvokeSource } from "./invoke-dispatch.js";
|
|
@@ -62,6 +65,7 @@ import {
|
|
|
62
65
|
} from "./manifest-sources/analysis-stamp.js";
|
|
63
66
|
import {
|
|
64
67
|
cachePathForCanonical,
|
|
68
|
+
legacyManifestsDirFallback,
|
|
65
69
|
resolveCacheRoot,
|
|
66
70
|
resolveEntryDir,
|
|
67
71
|
} from "./manifest-sources/local-manifest-cache-source.js";
|
|
@@ -418,19 +422,20 @@ export class Kernel implements IKernel {
|
|
|
418
422
|
options?.cacheDir !== undefined ? options.cacheDir : resolveCacheRoot(sourceUrl);
|
|
419
423
|
this._cacheRoot = cacheRoot;
|
|
420
424
|
const manifestsDir = cacheRoot ? `${cacheRoot}/manifests` : undefined;
|
|
425
|
+
const analysisDir = cacheRoot ? `${cacheRoot}/analysis` : undefined;
|
|
421
426
|
// `writeCache: false` (`telo run --no-cache-write`) keeps the cache
|
|
422
427
|
// READ-only: compiled validators and the analysis stamp are still loaded
|
|
423
428
|
// from disk, but never written back — so an ephemeral, read-only session
|
|
424
429
|
// rootfs validates in-memory without touching the baked cache.
|
|
425
430
|
const writeCache = options?.writeCache !== false;
|
|
426
431
|
// Point the shared schema validator at the cache so compiled AJV validators
|
|
427
|
-
// are loaded (and, when writable, persisted) under
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
);
|
|
432
|
+
// are loaded (and, when writable, persisted) under `<cache-root>/validators/`.
|
|
433
|
+
// Beside `manifests/` rather than inside it: a compiled validator is not a
|
|
434
|
+
// cached module manifest. Memory-/HTTP-rooted entries skip the cache; their
|
|
435
|
+
// schema compiles stay in-process only.
|
|
436
|
+
this.sharedSchemaValidator.setCacheDir(cacheRoot ? `${cacheRoot}/validators` : undefined, {
|
|
437
|
+
write: writeCache,
|
|
438
|
+
});
|
|
434
439
|
this.rootContext = new ModuleContext(
|
|
435
440
|
sourceUrl,
|
|
436
441
|
{},
|
|
@@ -548,9 +553,7 @@ export class Kernel implements IKernel {
|
|
|
548
553
|
// passes are elided. Memory- / HTTP-rooted entries have no
|
|
549
554
|
// local stamp store and always re-validate.
|
|
550
555
|
const analysisSignature = computeAnalysisSignature(analysisGraph);
|
|
551
|
-
const stamp =
|
|
552
|
-
? await readAnalysisStamp("", manifestsDir)
|
|
553
|
-
: undefined;
|
|
556
|
+
const stamp = analysisDir ? await readAnalysisStamp(sourceUrl, analysisDir) : undefined;
|
|
554
557
|
const skipValidation = stamp?.signature === analysisSignature;
|
|
555
558
|
const errors = this.analyzer.analyzeErrors(
|
|
556
559
|
staticManifests,
|
|
@@ -571,13 +574,13 @@ export class Kernel implements IKernel {
|
|
|
571
574
|
errors.map(staticDiagnosticToRuntime),
|
|
572
575
|
);
|
|
573
576
|
}
|
|
574
|
-
if (
|
|
577
|
+
if (analysisDir && writeCache && !skipValidation) {
|
|
575
578
|
// Best-effort: stamp the verdict so subsequent loads hit the fast
|
|
576
579
|
// path. A read-only filesystem (baked Docker image) reports the
|
|
577
580
|
// failure on stderr and keeps running — the lookup above will
|
|
578
581
|
// simply miss next time. Skipped under `--no-cache-write`.
|
|
579
582
|
try {
|
|
580
|
-
await writeAnalysisStamp(
|
|
583
|
+
await writeAnalysisStamp(sourceUrl, analysisSignature, analysisDir);
|
|
581
584
|
} catch (err) {
|
|
582
585
|
this.logging.kernelLogger().warn("analysis stamp write failed", undefined, { error: err });
|
|
583
586
|
}
|
|
@@ -1057,6 +1060,11 @@ export class Kernel implements IKernel {
|
|
|
1057
1060
|
this.siblingLibraries.clear();
|
|
1058
1061
|
const transports = defaultTransportRegistry(this.registryUrl);
|
|
1059
1062
|
const entryDir = this._entryUrl ? resolveEntryDir(this._entryUrl) ?? "" : "";
|
|
1063
|
+
// The same pre-anchor root `LocalManifestCacheSource` falls back to. Layers
|
|
1064
|
+
// live beside the cached manifest, so both halves have to look in the same
|
|
1065
|
+
// place or an offline upgrade resolves a manifest from disk and then fetches
|
|
1066
|
+
// its controllers.
|
|
1067
|
+
const legacyDir = legacyManifestsDirFallback(entryDir, manifestsDir ?? null);
|
|
1060
1068
|
// Parsed once per module and shared with the sibling-library join below: a
|
|
1061
1069
|
// `telo.yaml` is a large multi-document file, and re-reading each target's
|
|
1062
1070
|
// once per import edge put dozens of redundant full-YAML parses on the boot
|
|
@@ -1074,6 +1082,7 @@ export class Kernel implements IKernel {
|
|
|
1074
1082
|
entryDir,
|
|
1075
1083
|
this.registryUrl,
|
|
1076
1084
|
manifestsDir,
|
|
1085
|
+
legacyDir,
|
|
1077
1086
|
);
|
|
1078
1087
|
directories.set(file.source, moduleDir ?? undefined);
|
|
1079
1088
|
const artifact = moduleArtifactFor({
|
|
@@ -1417,6 +1426,18 @@ export class Kernel implements IKernel {
|
|
|
1417
1426
|
resolvedKind,
|
|
1418
1427
|
(processedResource.metadata?.name as string | undefined) ?? "<unnamed>",
|
|
1419
1428
|
);
|
|
1429
|
+
recordInstanceDeclaration(instance, processedResource);
|
|
1430
|
+
// Stamp the DECLARATION SITE here rather than at Phase-5 injection, because
|
|
1431
|
+
// this is the only point where the instance and the context that declared it
|
|
1432
|
+
// are both in hand: injection sees the consumer's context, not the target's,
|
|
1433
|
+
// so a module stamped there would be whoever referenced it. Without it a
|
|
1434
|
+
// durable step could not name its target anywhere the instance does not
|
|
1435
|
+
// exist — the whole reason `step()` takes an identity at all.
|
|
1436
|
+
const originPointer = originPointerOf(processedResource);
|
|
1437
|
+
stampRefIdentity(instance, resolvedKind, processedResource.metadata?.name ?? "<unnamed>", {
|
|
1438
|
+
...(moduleCtx?.source ? { module: moduleCtx.source } : {}),
|
|
1439
|
+
...(originPointer === undefined ? {} : { pointer: originPointer }),
|
|
1440
|
+
});
|
|
1420
1441
|
(ctx as ResourceContextImpl).bindResourceIdentity(handle, resolvedKind, processedResource);
|
|
1421
1442
|
|
|
1422
1443
|
// Bind the resolved invocation contract to the instance, here at the kernel's
|
|
@@ -1491,8 +1512,43 @@ export class Kernel implements IKernel {
|
|
|
1491
1512
|
const resolveDef: DefResolver = (kind, from) =>
|
|
1492
1513
|
this.registry.resolveDefinitionIn(kind, from?.metadata?.module);
|
|
1493
1514
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1515
|
+
// A DECLARATION-derived slot is resolved against the context that OWNS this
|
|
1516
|
+
// resource, so a bare name means the same thing here as it does to `!ref`
|
|
1517
|
+
// and to CEL — scope-local first, enclosing module as the fallback — and an
|
|
1518
|
+
// alias routes into that import's exported instances.
|
|
1519
|
+
const projections: ProjectionScope = {
|
|
1520
|
+
resolveDefinition: (kind) =>
|
|
1521
|
+
this.controllers.getDefinition(kind) as unknown as Record<string, any> | undefined,
|
|
1522
|
+
// The slot holds the LIVE INSTANCE by now — Phase-5 injection runs before
|
|
1523
|
+
// create — so the declaration is recovered by instance identity. The ref
|
|
1524
|
+
// shape is still accepted, because a ref slot on a `with:`-scoped resource
|
|
1525
|
+
// is not an injection site and reaches the controller unresolved.
|
|
1526
|
+
resolveManifest: (value) => {
|
|
1527
|
+
const injected = declarationOfInstance(value);
|
|
1528
|
+
if (injected) return { manifest: injected as unknown as Record<string, any> };
|
|
1529
|
+
const ref = readProjectionRef(value);
|
|
1530
|
+
if (!ref) return undefined;
|
|
1531
|
+
const found = impl.resolveDeclaredManifest(ref.name, ref.alias);
|
|
1532
|
+
return found ? { manifest: found as unknown as Record<string, any> } : undefined;
|
|
1533
|
+
},
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
const input = resolveBoundContract(
|
|
1537
|
+
"inputType",
|
|
1538
|
+
resource,
|
|
1539
|
+
definition,
|
|
1540
|
+
resolveDef,
|
|
1541
|
+
factory,
|
|
1542
|
+
projections,
|
|
1543
|
+
);
|
|
1544
|
+
const output = resolveBoundContract(
|
|
1545
|
+
"outputType",
|
|
1546
|
+
resource,
|
|
1547
|
+
definition,
|
|
1548
|
+
resolveDef,
|
|
1549
|
+
factory,
|
|
1550
|
+
projections,
|
|
1551
|
+
);
|
|
1496
1552
|
if (!input && !output) return;
|
|
1497
1553
|
|
|
1498
1554
|
bindContract(instance, {
|
|
@@ -1576,3 +1632,28 @@ export class Kernel implements IKernel {
|
|
|
1576
1632
|
);
|
|
1577
1633
|
}
|
|
1578
1634
|
}
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* The JSON pointer to an inline declaration, from the origin the loader stamped.
|
|
1638
|
+
*
|
|
1639
|
+
* `xTeloOrigin.pathFromParent` is a dotted path with `[N]` indices — the loader's
|
|
1640
|
+
* own spelling, which addresses the manifest tree and nothing else. A pointer is
|
|
1641
|
+
* what crosses a boundary, so the conversion happens here rather than being left
|
|
1642
|
+
* to each recipient to reinvent: segments are identifiers and indices, so the
|
|
1643
|
+
* rewrite is mechanical and total.
|
|
1644
|
+
*/
|
|
1645
|
+
function originPointerOf(resource: ResourceManifest): string | undefined {
|
|
1646
|
+
const origin = resource.metadata?.xTeloOrigin as
|
|
1647
|
+
| { parentName?: unknown; pathFromParent?: unknown }
|
|
1648
|
+
| undefined;
|
|
1649
|
+
if (typeof origin?.parentName !== "string" || typeof origin.pathFromParent !== "string") {
|
|
1650
|
+
return undefined;
|
|
1651
|
+
}
|
|
1652
|
+
const pointer = origin.pathFromParent
|
|
1653
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
1654
|
+
.split(".")
|
|
1655
|
+
.filter((segment) => segment.length > 0)
|
|
1656
|
+
.map((segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1"))
|
|
1657
|
+
.join("/");
|
|
1658
|
+
return `${origin.parentName}#/${pointer}`;
|
|
1659
|
+
}
|
package/src/manifest-schemas.ts
CHANGED
|
@@ -7,19 +7,30 @@ import * as path from "path";
|
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Hash-keyed analysis cache: a tiny JSON sidecar
|
|
10
|
+
* Hash-keyed analysis cache: a tiny JSON sidecar under `.telo/analysis/`
|
|
11
11
|
* recording that an exact set of manifest bytes — under specific
|
|
12
12
|
* `@telorun/kernel` and `@telorun/analyzer` package versions — passed
|
|
13
13
|
* `analyzer.analyzeErrors`. The next `kernel.load` reads the sidecar
|
|
14
14
|
* and, if signatures match, skips the per-resource validation walk.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* ONE STAMP PER ENTRY, in a directory keyed by a hash of the entry URL.
|
|
17
|
+
* A single file holding a single signature was per-app only because every app
|
|
18
|
+
* used to get its own `.telo` beside its manifest; once the cache root is shared
|
|
19
|
+
* across a workspace, one file means each app overwrites the last — A stamps, B
|
|
20
|
+
* misses and overwrites, forever. That is a permanent 100% miss with no error to
|
|
21
|
+
* show for it, worst in the test suite, where a kernel is spawned per manifest
|
|
22
|
+
* and the cache matters most.
|
|
23
|
+
*
|
|
24
|
+
* A DIRECTORY rather than several records in one file, because two kernels
|
|
25
|
+
* loading different manifests concurrently would otherwise read-modify-write the
|
|
26
|
+
* same JSON and lose each other's entry — and concurrent loads are the normal
|
|
27
|
+
* case, not the exception.
|
|
28
|
+
*
|
|
29
|
+
* NOT under `manifests/`, which holds cached module manifests keyed by transport.
|
|
30
|
+
* A verdict about an entry is not a manifest; filing it there made the directory
|
|
31
|
+
* mean two things.
|
|
20
32
|
*/
|
|
21
33
|
|
|
22
|
-
const CACHE_SUBDIR = ".telo/manifests";
|
|
23
34
|
|
|
24
35
|
/** File-format version of the analysis stamp envelope. Only bumped when
|
|
25
36
|
* the on-disk *layout* changes (new fields, restructured payload). The
|
|
@@ -30,7 +41,6 @@ const CACHE_SUBDIR = ".telo/manifests";
|
|
|
30
41
|
* disk. A hand-maintained integer for that purpose would silently mask
|
|
31
42
|
* newly-stricter validation until the next manifest edit. */
|
|
32
43
|
const ANALYSIS_STAMP_FORMAT_VERSION = 1;
|
|
33
|
-
const ANALYSIS_STAMP_FILE = `${CACHE_SUBDIR}/.validated.json`;
|
|
34
44
|
|
|
35
45
|
const localRequire = createRequire(import.meta.url);
|
|
36
46
|
|
|
@@ -128,21 +138,33 @@ export function computeAnalysisSignature(graph: LoadedGraph): string {
|
|
|
128
138
|
.digest("hex");
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
141
|
+
/** Where one entry's stamp lives: `<analysisDir>/<hash of entry URL>.json`.
|
|
142
|
+
*
|
|
143
|
+
* Keyed by the entry URL rather than by its directory, because two manifests in
|
|
144
|
+
* one directory (an app and its test harness) are two entries with two verdicts,
|
|
145
|
+
* and a shared cache root makes the directory a far weaker discriminator than it
|
|
146
|
+
* used to be. */
|
|
147
|
+
function stampPath(analysisDir: string, entryUrl: string): string {
|
|
148
|
+
const id = createHash("sha256").update(entryUrl).digest("hex").slice(0, 32);
|
|
149
|
+
return path.join(analysisDir, `${id}.json`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Read the stamped analysis verdict for `entryUrl`, or `undefined` when
|
|
153
|
+
* missing / unreadable / format-mismatched. The `version` field is the
|
|
154
|
+
* on-disk *format* version; semantic invalidation flows through the
|
|
155
|
+
* signature (which embeds package versions). A future format change bumps
|
|
156
|
+
* `version` so older kernels reading a newer stamp (or vice versa) discard
|
|
157
|
+
* rather than misparse.
|
|
158
|
+
*
|
|
159
|
+
* A pre-workspace-anchor `.telo/manifests/.validated.json` is simply never
|
|
160
|
+
* looked at: it is a file where this layout wants a directory, so neither
|
|
161
|
+
* version of the kernel can misread the other's. */
|
|
137
162
|
export async function readAnalysisStamp(
|
|
138
|
-
|
|
139
|
-
|
|
163
|
+
entryUrl: string,
|
|
164
|
+
analysisDir: string,
|
|
140
165
|
): Promise<AnalysisStamp | undefined> {
|
|
141
|
-
const stampPath = manifestsDir
|
|
142
|
-
? path.join(manifestsDir, ".validated.json")
|
|
143
|
-
: path.join(entryDir, ANALYSIS_STAMP_FILE);
|
|
144
166
|
try {
|
|
145
|
-
const text = await fs.readFile(stampPath, "utf-8");
|
|
167
|
+
const text = await fs.readFile(stampPath(analysisDir, entryUrl), "utf-8");
|
|
146
168
|
const parsed = JSON.parse(text) as Partial<AnalysisStamp>;
|
|
147
169
|
if (
|
|
148
170
|
parsed?.version === ANALYSIS_STAMP_FORMAT_VERSION &&
|
|
@@ -160,17 +182,23 @@ export async function readAnalysisStamp(
|
|
|
160
182
|
* per-resource validation walk when the manifest set is unchanged.
|
|
161
183
|
* Idempotent; safe to call after every successful load. */
|
|
162
184
|
export async function writeAnalysisStamp(
|
|
163
|
-
|
|
185
|
+
entryUrl: string,
|
|
164
186
|
signature: string,
|
|
165
|
-
|
|
187
|
+
analysisDir: string,
|
|
166
188
|
): Promise<void> {
|
|
167
189
|
const stamp: AnalysisStamp = {
|
|
168
190
|
version: ANALYSIS_STAMP_FORMAT_VERSION,
|
|
169
191
|
signature,
|
|
170
192
|
};
|
|
171
|
-
const target =
|
|
172
|
-
? path.join(manifestsDir, ".validated.json")
|
|
173
|
-
: path.join(entryDir, ANALYSIS_STAMP_FILE);
|
|
193
|
+
const target = stampPath(analysisDir, entryUrl);
|
|
174
194
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
175
|
-
|
|
195
|
+
// Temp file + rename, as the controller-source cache does. Two kernels loading
|
|
196
|
+
// the SAME entry concurrently — the normal case for a shared root, where a test
|
|
197
|
+
// suite runs many manifests at once — would otherwise interleave writes to one
|
|
198
|
+
// path. A torn stamp is read as a miss rather than as a wrong verdict, so the
|
|
199
|
+
// cost is a silent re-validation, but rename makes it unrepresentable for the
|
|
200
|
+
// price of one syscall.
|
|
201
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
202
|
+
await fs.writeFile(tmp, JSON.stringify(stamp), "utf-8");
|
|
203
|
+
await fs.rename(tmp, target);
|
|
176
204
|
}
|
|
@@ -12,6 +12,7 @@ import { fileURLToPath, pathToFileURL } from "url";
|
|
|
12
12
|
|
|
13
13
|
import { hostEnv } from "../host-env.js";
|
|
14
14
|
import { TransportRegistry, defaultTransportRegistry } from "../transports/transport-registry.js";
|
|
15
|
+
import { findWorkspaceRoot } from "../workspace-marker.js";
|
|
15
16
|
|
|
16
17
|
const CACHE_SUBDIR = ".telo/manifests";
|
|
17
18
|
const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
|
|
@@ -51,15 +52,51 @@ function cachePathForUrl(
|
|
|
51
52
|
return joinUnder(cacheRoot, ...key.split("/"));
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/** The PRE-WORKSPACE-ANCHOR manifest cache for an entry: always
|
|
56
|
+
* `<entry-dir>/.telo/manifests`, regardless of marker or env override. `null`
|
|
57
|
+
* for an entry with no local anchor.
|
|
58
|
+
*
|
|
59
|
+
* A single definition because two things read the old location and they must
|
|
60
|
+
* agree: the manifest source serves `telo.yaml` from it, and `moduleDirectoryFor`
|
|
61
|
+
* places that module's LAYERS beside it. Deriving the path twice is how the two
|
|
62
|
+
* halves end up disagreeing — the manifest resolving from the old root while its
|
|
63
|
+
* controller layers are looked for under the new one, which is exactly the
|
|
64
|
+
* offline boot the fallback exists to keep working. */
|
|
65
|
+
export function legacyManifestsDir(entryDir: string): string | null {
|
|
66
|
+
return entryDir ? path.join(entryDir, CACHE_SUBDIR) : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `legacyManifestsDir`, or `null` when it would coincide with `current` — so the
|
|
70
|
+
* fallback is never a second lookup at the same place. */
|
|
71
|
+
export function legacyManifestsDirFallback(
|
|
72
|
+
entryDir: string,
|
|
73
|
+
current: string | null,
|
|
74
|
+
): string | null {
|
|
75
|
+
const legacy = legacyManifestsDir(entryDir);
|
|
76
|
+
if (legacy === null) return null;
|
|
77
|
+
if (current !== null && path.resolve(legacy) === path.resolve(current)) return null;
|
|
78
|
+
return legacy;
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
/**
|
|
55
|
-
* Reads previously-cached manifest YAMLs from
|
|
56
|
-
*
|
|
57
|
-
*
|
|
82
|
+
* Reads previously-cached manifest YAMLs from the resolved manifest cache. Sits
|
|
83
|
+
* ahead of `RegistrySource` / `HttpSource` in the source chain — a hit makes boot
|
|
84
|
+
* hermetic, a miss falls through to the network source unchanged.
|
|
58
85
|
*
|
|
59
86
|
* Populated by `writeManifestCache` at install time.
|
|
87
|
+
*
|
|
88
|
+
* On a miss it consults the PRE-WORKSPACE-ANCHOR location
|
|
89
|
+
* (`<entry-dir>/.telo/manifests/`) before giving up. Every other cache in `.telo`
|
|
90
|
+
* costs only CPU when it goes cold; this one costs network, so without the
|
|
91
|
+
* fallback the move to a workspace-anchored root would stop a hermetic setup from
|
|
92
|
+
* booting — its `telo install` output stranded at the old path, with the failure
|
|
93
|
+
* surfacing as a registry fetch on a machine that has no route to one. Read-only
|
|
94
|
+
* and one directory deep: writes always go to the current root, so the old copy
|
|
95
|
+
* ages out rather than being maintained.
|
|
60
96
|
*/
|
|
61
97
|
export class LocalManifestCacheSource implements ManifestSource {
|
|
62
|
-
private readonly cacheRoot: string;
|
|
98
|
+
private readonly cacheRoot: string | null;
|
|
99
|
+
private readonly legacyRoot: string | null;
|
|
63
100
|
private readonly transports: TransportRegistry;
|
|
64
101
|
|
|
65
102
|
constructor(
|
|
@@ -70,7 +107,8 @@ export class LocalManifestCacheSource implements ManifestSource {
|
|
|
70
107
|
// `manifestsDir` is the resolved manifest-cache directory threaded from a
|
|
71
108
|
// single `resolveCacheRoot` (honours `TELO_CACHE_DIR`); when absent we fall
|
|
72
109
|
// back to the entry-anchored default so library/test callers are unchanged.
|
|
73
|
-
this.cacheRoot = manifestsDir ??
|
|
110
|
+
this.cacheRoot = manifestsDir ?? legacyManifestsDir(entryDir);
|
|
111
|
+
this.legacyRoot = legacyManifestsDirFallback(entryDir, this.cacheRoot);
|
|
74
112
|
this.transports = defaultTransportRegistry(registryUrl);
|
|
75
113
|
}
|
|
76
114
|
|
|
@@ -108,7 +146,12 @@ export class LocalManifestCacheSource implements ManifestSource {
|
|
|
108
146
|
}
|
|
109
147
|
|
|
110
148
|
private tryMap(url: string): string | null {
|
|
111
|
-
|
|
149
|
+
return this.tryMapIn(url, this.cacheRoot) ?? this.tryMapIn(url, this.legacyRoot);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private tryMapIn(url: string, root: string | null): string | null {
|
|
153
|
+
if (root === null) return null;
|
|
154
|
+
const candidate = cachePathForUrl(url, root, this.transports);
|
|
112
155
|
if (!candidate) return null;
|
|
113
156
|
// Require a regular file. A directory, dangling symlink, or stat failure
|
|
114
157
|
// (ENOENT, EACCES, EISDIR-on-component) all fall through as a cache miss
|
|
@@ -219,18 +262,30 @@ export function resolveEntryDir(entryPath: string): string | null {
|
|
|
219
262
|
}
|
|
220
263
|
|
|
221
264
|
/** The single `.telo` cache root for an entry, resolved once and threaded to
|
|
222
|
-
* every consumer (manifest cache, compiled validators, analysis
|
|
223
|
-
* install root) so none of them re-derive it or read the env
|
|
265
|
+
* every consumer (manifest cache, compiled validators, analysis stamps, npm
|
|
266
|
+
* install root, cargo target dirs) so none of them re-derive it or read the env
|
|
267
|
+
* independently.
|
|
268
|
+
*
|
|
269
|
+
* Precedence: `TELO_CACHE_DIR` (the relocated root a prebuilt image bakes its
|
|
270
|
+
* deps into) wins; then the directory holding `telo-workspace.yaml`, so every
|
|
271
|
+
* app in one repo shares a cache instead of each carrying its own copy of the
|
|
272
|
+
* same manifests, validators, bundles and npm tree; then `<entry-dir>/.telo`.
|
|
273
|
+
*
|
|
274
|
+
* Anchoring on the marker's LOCATION only — never its `modules:` list, which is
|
|
275
|
+
* release scope — so a manifest in no release subtree (an example, a test
|
|
276
|
+
* fixture) shares the cache exactly as an app does. With no marker anywhere
|
|
277
|
+
* above, this collapses to what it did before the anchor existed, so the file
|
|
278
|
+
* enables the shared cache rather than gating one and deleting it cannot break
|
|
279
|
+
* a build.
|
|
224
280
|
*
|
|
225
|
-
* `TELO_CACHE_DIR` (the relocated root a prebuilt image bakes its deps into)
|
|
226
|
-
* wins; otherwise the root sits beside the entry at `<entry-dir>/.telo`.
|
|
227
281
|
* Returns `null` for an entry with no local anchor — an http(s), `memory://`
|
|
228
282
|
* or any other non-`file:` scheme — in which case the disk cache is skipped.
|
|
229
|
-
* Consumers append the conventional subdirs: `manifests/`, `
|
|
230
|
-
* `npm/`. */
|
|
283
|
+
* Consumers append the conventional subdirs: `manifests/`, `analysis/`,
|
|
284
|
+
* `validators/`, `controller-src/`, `npm/`. */
|
|
231
285
|
export function resolveCacheRoot(entryPath: string): string | null {
|
|
232
286
|
const override = hostEnv().TELO_CACHE_DIR;
|
|
233
287
|
if (override && override.trim()) return path.resolve(override.trim());
|
|
234
288
|
const entryDir = resolveEntryDir(entryPath);
|
|
235
|
-
|
|
289
|
+
if (!entryDir) return null;
|
|
290
|
+
return path.join(findWorkspaceRoot(entryDir) ?? entryDir, ".telo");
|
|
236
291
|
}
|
package/src/module-context.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
InvokeStepContext,
|
|
16
16
|
ModuleContext as IModuleContext,
|
|
17
17
|
ResourceInstance,
|
|
18
|
+
ResourceManifest,
|
|
18
19
|
} from "@telorun/sdk";
|
|
19
20
|
import type { EmitEvent, InstanceFactory } from "@telorun/sdk";
|
|
20
21
|
import { EvaluationContext } from "./evaluation-context.js";
|
|
@@ -106,7 +107,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
106
107
|
names: Set<string>;
|
|
107
108
|
terminal: (
|
|
108
109
|
name: string,
|
|
109
|
-
) =>
|
|
110
|
+
) =>
|
|
111
|
+
| (() =>
|
|
112
|
+
| { kind: string; instance: ResourceInstance; manifest?: ResourceManifest }
|
|
113
|
+
| undefined)
|
|
114
|
+
| undefined;
|
|
110
115
|
}
|
|
111
116
|
>();
|
|
112
117
|
|
|
@@ -292,7 +297,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
292
297
|
names: string[],
|
|
293
298
|
terminal: (
|
|
294
299
|
name: string,
|
|
295
|
-
) =>
|
|
300
|
+
) =>
|
|
301
|
+
| (() =>
|
|
302
|
+
| { kind: string; instance: ResourceInstance; manifest?: ResourceManifest }
|
|
303
|
+
| undefined)
|
|
304
|
+
| undefined,
|
|
296
305
|
): void {
|
|
297
306
|
this.importedScopes.set(alias, { names: new Set(names), terminal });
|
|
298
307
|
}
|
|
@@ -369,7 +378,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
369
378
|
const kind = rawKind.startsWith("Self.")
|
|
370
379
|
? `${moduleName}.${rawKind.slice("Self.".length)}`
|
|
371
380
|
: rawKind;
|
|
372
|
-
|
|
381
|
+
// The manifest rides along because a DECLARATION-derived contract
|
|
382
|
+
// (`x-telo-schema-projection-from`) reads the target's own declaration,
|
|
383
|
+
// and a re-export forwards this getter verbatim — so carrying it here
|
|
384
|
+
// makes it reachable at any depth with no second table.
|
|
385
|
+
return { kind, instance: inst.instance, manifest: inst.resource };
|
|
373
386
|
});
|
|
374
387
|
}
|
|
375
388
|
for (const k of kindEntries) {
|
|
@@ -407,6 +420,14 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
407
420
|
return scope.terminal(name)?.()?.instance;
|
|
408
421
|
}
|
|
409
422
|
|
|
423
|
+
/** The manifest half of `resolveImportedInstance` — what a name was DECLARED
|
|
424
|
+
* with, for a contract typed from a referenced declaration. */
|
|
425
|
+
override resolveImportedManifest(alias: string, name: string): ResourceManifest | undefined {
|
|
426
|
+
const scope = this.importedScopes.get(alias);
|
|
427
|
+
if (!scope || !scope.names.has(name)) return undefined;
|
|
428
|
+
return scope.terminal(name)?.()?.manifest;
|
|
429
|
+
}
|
|
430
|
+
|
|
410
431
|
/** Like `resolveImportedInstance`, but returns the `{kind, name}` ref (canonical kind)
|
|
411
432
|
* for controllers that resolve step/handler invokes to refs rather than live instances
|
|
412
433
|
* (e.g. Run.Sequence via `ensureKindRef`). The alias is reattached by the caller. */
|
package/src/resource-context.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type ResourceHandle,
|
|
27
27
|
type RuntimeSeam,
|
|
28
28
|
type TypeRule,
|
|
29
|
+
type OpenZoneAttributes,
|
|
29
30
|
type ZoneEntry,
|
|
30
31
|
} from "@telorun/sdk";
|
|
31
32
|
import { registerTeloKeywords } from "@telorun/analyzer";
|
|
@@ -426,6 +427,10 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
426
427
|
return this.zoneContext().zonesFor(instance, ctx);
|
|
427
428
|
}
|
|
428
429
|
|
|
430
|
+
zoneAttributes(ctx?: InvokeContext): readonly OpenZoneAttributes[] {
|
|
431
|
+
return this.zoneContext().zoneAttributes(ctx);
|
|
432
|
+
}
|
|
433
|
+
|
|
429
434
|
/** The root context for runtime-driven inbound work — inherits nothing from
|
|
430
435
|
* whatever ambient happens to be live at the registration site. */
|
|
431
436
|
rootContext(opts?: { cancellation?: CancellationSource }): InvokeContext {
|
|
@@ -514,6 +519,19 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
514
519
|
return this.moduleContext.resolveImportedInstance(alias, name);
|
|
515
520
|
}
|
|
516
521
|
|
|
522
|
+
/**
|
|
523
|
+
* The manifest a name was DECLARED with, resolved in the context that OWNS
|
|
524
|
+
* this resource — scope-local first, enclosing module as the fallback, an
|
|
525
|
+
* alias routing into that import's exports. The same order `resolveRef` and
|
|
526
|
+
* the CEL `resources` layering use, so a declaration lookup cannot disagree
|
|
527
|
+
* with them about what a name means.
|
|
528
|
+
*
|
|
529
|
+
* Used to resolve a DECLARATION-derived contract slot at bind time.
|
|
530
|
+
*/
|
|
531
|
+
resolveDeclaredManifest(name: string, alias?: string): ResourceManifest | undefined {
|
|
532
|
+
return this.owningContext.resolveDeclaredManifest?.(name, alias);
|
|
533
|
+
}
|
|
534
|
+
|
|
517
535
|
resolveRef<T>(
|
|
518
536
|
value: unknown,
|
|
519
537
|
guard: (candidate: unknown) => candidate is T,
|
package/src/runtime-seam.ts
CHANGED
|
@@ -176,7 +176,15 @@ export class KernelRuntimeSeam implements RuntimeSeam {
|
|
|
176
176
|
// child down in its own `finally`, so this only has to close the channels.
|
|
177
177
|
const exitCode = (async () => {
|
|
178
178
|
try {
|
|
179
|
-
|
|
179
|
+
// The child runs a manifest in the SAME workspace as its parent, so it
|
|
180
|
+
// shares the root the parent already resolved instead of deriving one
|
|
181
|
+
// beside the child manifest. Deriving put a `.telo` in every directory
|
|
182
|
+
// holding a test manifest and rebuilt every controller bundle once per
|
|
183
|
+
// test, since `Test.Suite` runs each test through here.
|
|
184
|
+
//
|
|
185
|
+
// `undefined` means "resolve one yourself" and `null` means "no cache" —
|
|
186
|
+
// a parent with no local anchor must yield the first, not the second.
|
|
187
|
+
await child.load(source, { cacheDir: this.kernel.getCacheRoot() ?? undefined });
|
|
180
188
|
await child.start();
|
|
181
189
|
return child.exitCode;
|
|
182
190
|
} catch (err) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding `telo-workspace.yaml` on disk.
|
|
3
|
+
*
|
|
4
|
+
* Its LOCATION is a general anchor — the directory workspace-relative paths are
|
|
5
|
+
* measured from, the outer bound on how far a walk-up may look, and the root the
|
|
6
|
+
* `.telo` cache is anchored at — while its `modules:` list is release scope. So
|
|
7
|
+
* the finder is deliberately separate from `release/`: `telo run` bounds its
|
|
8
|
+
* `.env` collection with the marker and `resolveCacheRoot` anchors on it, and
|
|
9
|
+
* neither may reach into the release namespace to do so, where `loadWorkspace`
|
|
10
|
+
* (which parses `modules:` and throws when the file is absent) sits one import
|
|
11
|
+
* away.
|
|
12
|
+
*
|
|
13
|
+
* It lives in the KERNEL rather than the CLI because the cache root is resolved
|
|
14
|
+
* here, on a path a CLI-less embedder also takes (a child kernel started through
|
|
15
|
+
* the runtime seam, a test harness). The CLI re-exports it; nothing is duplicated.
|
|
16
|
+
*
|
|
17
|
+
* Parsing the file is the analyzer's (`release/workspace-config.ts`); finding it
|
|
18
|
+
* is the Node half, and this is all of it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { WORKSPACE_FILENAME } from "@telorun/analyzer";
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
|
|
25
|
+
export { WORKSPACE_FILENAME };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Walk up from `from` looking for the marker. Returns the directory holding it,
|
|
29
|
+
* or `undefined` — the file is optional. `telo release` requires one and reports
|
|
30
|
+
* its absence; `telo run` and `resolveCacheRoot` read `undefined` as "no parent
|
|
31
|
+
* lookup", falling back to what they did before the marker existed.
|
|
32
|
+
*
|
|
33
|
+
* `from` is resolved through symlinks first: a manifest reached through a linked
|
|
34
|
+
* directory would otherwise walk the LINK's parents, miss a marker that is right
|
|
35
|
+
* there in the real tree, and silently fall back to a narrower answer.
|
|
36
|
+
*/
|
|
37
|
+
export function findWorkspaceRoot(from: string): string | undefined {
|
|
38
|
+
let dir = realPath(path.resolve(from));
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (isFile(path.join(dir, WORKSPACE_FILENAME))) return dir;
|
|
41
|
+
const parent = path.dirname(dir);
|
|
42
|
+
if (parent === dir) return undefined;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The marker must be a FILE. `existsSync` would accept a directory of that name
|
|
48
|
+
* and anchor the whole cache on it, and the Rust half tests `is_file()` — a
|
|
49
|
+
* workspace visible to one kernel and not the other is precisely the divergence
|
|
50
|
+
* a shared marker exists to prevent. */
|
|
51
|
+
function isFile(target: string): boolean {
|
|
52
|
+
try {
|
|
53
|
+
return fs.statSync(target).isFile();
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `fs.realpathSync`, falling back to the input when the path does not exist —
|
|
60
|
+
* a manifest path that is wrong is the caller's error to report, not this
|
|
61
|
+
* helper's to convert into a different one. */
|
|
62
|
+
export function realPath(target: string): string {
|
|
63
|
+
try {
|
|
64
|
+
return fs.realpathSync(target);
|
|
65
|
+
} catch {
|
|
66
|
+
return target;
|
|
67
|
+
}
|
|
68
|
+
}
|