@ingestron/core 0.12.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.
Files changed (84) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +56 -0
  4. package/SECURITY.md +44 -0
  5. package/THIRD_PARTY_NOTICES.md +693 -0
  6. package/dist/assets/schemas/ODCS-LICENSE +201 -0
  7. package/dist/assets/schemas/odcs-3.1.0.json +2929 -0
  8. package/dist/assets/schemas/provenance.json +16 -0
  9. package/dist/compiler-fingerprint.json +84 -0
  10. package/dist/core/authoring.d.ts +55 -0
  11. package/dist/core/authoring.js +315 -0
  12. package/dist/core/config.d.ts +34 -0
  13. package/dist/core/config.js +233 -0
  14. package/dist/core/connections.d.ts +19 -0
  15. package/dist/core/connections.js +203 -0
  16. package/dist/core/connector-package.d.ts +37 -0
  17. package/dist/core/connector-package.js +43 -0
  18. package/dist/core/contracts.d.ts +8 -0
  19. package/dist/core/contracts.js +45 -0
  20. package/dist/core/deliveries.d.ts +16 -0
  21. package/dist/core/deliveries.js +28 -0
  22. package/dist/core/delivery-exports.d.ts +20 -0
  23. package/dist/core/delivery-exports.js +119 -0
  24. package/dist/core/development.d.ts +50 -0
  25. package/dist/core/development.js +94 -0
  26. package/dist/core/ecosystem-authoring.d.ts +13 -0
  27. package/dist/core/ecosystem-authoring.js +82 -0
  28. package/dist/core/errors.d.ts +12 -0
  29. package/dist/core/errors.js +43 -0
  30. package/dist/core/execution.d.ts +28 -0
  31. package/dist/core/execution.js +297 -0
  32. package/dist/core/extension-packs.d.ts +8 -0
  33. package/dist/core/extension-packs.js +34 -0
  34. package/dist/core/fingerprint.d.ts +1 -0
  35. package/dist/core/fingerprint.js +16 -0
  36. package/dist/core/generate.d.ts +16 -0
  37. package/dist/core/generate.js +198 -0
  38. package/dist/core/installation.d.ts +3 -0
  39. package/dist/core/installation.js +14 -0
  40. package/dist/core/installed-plugins.d.ts +41 -0
  41. package/dist/core/installed-plugins.js +92 -0
  42. package/dist/core/model-pack-schema.d.ts +46 -0
  43. package/dist/core/model-pack-schema.js +81 -0
  44. package/dist/core/model-packs.d.ts +4 -0
  45. package/dist/core/model-packs.js +33 -0
  46. package/dist/core/operations.d.ts +346 -0
  47. package/dist/core/operations.js +760 -0
  48. package/dist/core/package-references.d.ts +36 -0
  49. package/dist/core/package-references.js +103 -0
  50. package/dist/core/packages.d.ts +340 -0
  51. package/dist/core/packages.js +579 -0
  52. package/dist/core/planner.d.ts +114 -0
  53. package/dist/core/planner.js +821 -0
  54. package/dist/core/project-build.d.ts +13 -0
  55. package/dist/core/project-build.js +266 -0
  56. package/dist/core/provider-commands.d.ts +83 -0
  57. package/dist/core/provider-commands.js +252 -0
  58. package/dist/core/provider-starter.d.ts +5 -0
  59. package/dist/core/provider-starter.js +155 -0
  60. package/dist/core/report-pack-schema.d.ts +13 -0
  61. package/dist/core/report-pack-schema.js +15 -0
  62. package/dist/core/schema.d.ts +257 -0
  63. package/dist/core/schema.js +126 -0
  64. package/dist/core/workflows.d.ts +91 -0
  65. package/dist/core/workflows.js +438 -0
  66. package/dist/plugins/authoring.d.ts +88 -0
  67. package/dist/plugins/authoring.js +34 -0
  68. package/dist/plugins/compatibility.d.ts +23 -0
  69. package/dist/plugins/compatibility.js +55 -0
  70. package/dist/plugins/generation.d.ts +5 -0
  71. package/dist/plugins/generation.js +105 -0
  72. package/dist/plugins/provider-renderer.d.ts +7 -0
  73. package/dist/plugins/provider-renderer.js +37 -0
  74. package/dist/plugins/provider-worker.d.ts +1 -0
  75. package/dist/plugins/provider-worker.js +74 -0
  76. package/dist/sdk/adapter.d.ts +15 -0
  77. package/dist/sdk/adapter.js +15 -0
  78. package/dist/sdk/index.d.ts +4 -0
  79. package/dist/sdk/index.js +3 -0
  80. package/dist/sdk/schemas.d.ts +34 -0
  81. package/dist/sdk/schemas.js +74 -0
  82. package/dist/version.d.ts +5 -0
  83. package/dist/version.js +7 -0
  84. package/package.json +72 -0
@@ -0,0 +1,297 @@
1
+ /** Explicit execution boundary. Compilation never calls this module's launcher. */
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync, } from "node:fs";
3
+ import { resolve, relative } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { spawn } from "node:child_process";
6
+ import { parseEnv } from "node:util";
7
+ import { z } from "zod";
8
+ import { check, digest, canonical, Problem } from "./errors.js";
9
+ import { fence, resolvePackage, packageYaml, providerPackageSchema, } from "./packages.js";
10
+ import { validateOutput } from "./generate.js";
11
+ import { checkProviderCompatibility } from "../plugins/compatibility.js";
12
+ const id = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
13
+ const selection = {
14
+ from: z.string().optional(),
15
+ provider: id.optional(),
16
+ flow: id.optional(),
17
+ };
18
+ export const executionSchemas = {
19
+ runtime_prepare: z
20
+ .object({ ...selection, python: z.string().optional() })
21
+ .strict(),
22
+ run: z
23
+ .object({
24
+ ...selection,
25
+ action: z.enum(["discover", "review", "approve", "run"]).default("run"),
26
+ runId: id.optional(),
27
+ retry: id.optional(),
28
+ envFile: z.string().optional(),
29
+ })
30
+ .strict(),
31
+ run_status: z.object({ id }).strict(),
32
+ };
33
+ const now = () => new Date().toISOString();
34
+ function store(file, value) {
35
+ const temp = file + "." + randomUUID() + ".tmp";
36
+ writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", {
37
+ mode: 0o600,
38
+ flag: "wx",
39
+ });
40
+ renameSync(temp, file);
41
+ }
42
+ function alive(pid) {
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function receipt(root, runId) {
52
+ const file = fence(root, `.ingestron/runs/${runId}.json`);
53
+ check(existsSync(file), "RUN", "Unknown run identity");
54
+ const value = JSON.parse(readFileSync(file, "utf8"));
55
+ check(value.apiVersion === "ingestron.run/v1", "RUN", "Invalid run receipt");
56
+ if (value.status === "running" && !alive(value.pid))
57
+ value.status = "indeterminate";
58
+ return value;
59
+ }
60
+ async function launch(python, entry, request, cwd, env) {
61
+ return await new Promise((done, reject) => {
62
+ const child = spawn(python, [entry], {
63
+ cwd,
64
+ env,
65
+ stdio: ["pipe", "pipe", "pipe"],
66
+ detached: process.platform !== "win32",
67
+ });
68
+ let output = "", bytes = 0, stopped = false;
69
+ const signal = (s) => {
70
+ try {
71
+ process.platform === "win32"
72
+ ? child.kill(s)
73
+ : process.kill(-child.pid, s);
74
+ }
75
+ catch { }
76
+ };
77
+ let killTimer;
78
+ const stop = () => {
79
+ if (stopped)
80
+ return;
81
+ stopped = true;
82
+ signal("SIGINT");
83
+ killTimer = setTimeout(() => signal("SIGKILL"), 10000);
84
+ };
85
+ const timer = setTimeout(stop, 30 * 60 * 1000);
86
+ process.on("SIGINT", stop);
87
+ process.on("SIGTERM", stop);
88
+ const cleanup = () => {
89
+ clearTimeout(timer);
90
+ clearTimeout(killTimer);
91
+ process.off("SIGINT", stop);
92
+ process.off("SIGTERM", stop);
93
+ };
94
+ child.stdout.on("data", (chunk) => {
95
+ bytes += chunk.length;
96
+ if (bytes > 10_000_000)
97
+ stop();
98
+ else
99
+ output += chunk;
100
+ });
101
+ child.stderr.on("data", () => { }); // Source errors may contain credentials.
102
+ child.stdin.on("error", () => { });
103
+ child.on("error", () => {
104
+ cleanup();
105
+ reject(new Problem("RUNTIME", "Python could not start; install python3 for the local execution adapter"));
106
+ });
107
+ child.on("close", (code) => {
108
+ cleanup();
109
+ if (stopped) {
110
+ reject(new Problem("INTERRUPTED", "Execution interrupted or timed out; inspect its receipt before retrying"));
111
+ return;
112
+ }
113
+ try {
114
+ const result = JSON.parse(output);
115
+ check(result.apiVersion === "ingestron.execution-result/v1", "RUNTIME", "Invalid provider execution result");
116
+ check(code === 0 && result.status === "succeeded", "RUNTIME", result.message ?? "Provider execution failed");
117
+ done(result);
118
+ }
119
+ catch (error) {
120
+ reject(error instanceof Problem
121
+ ? error
122
+ : new Problem("RUNTIME", "Provider execution failed; upstream output withheld"));
123
+ }
124
+ });
125
+ child.stdin.end(JSON.stringify(request));
126
+ });
127
+ }
128
+ export async function performExecution(context, operation, args) {
129
+ const root = resolve(context.root), environment = context.environment ?? "dev";
130
+ if (operation === "run_status")
131
+ return receipt(root, args.id);
132
+ check(context.allowExecute && context.allowWrite, "PERMISSION", "Execution requires allowExecute and allowWrite in the operation context");
133
+ check(process.platform !== "win32", "CAPABILITY", "Local connector execution currently requires POSIX; use a supported Linux environment");
134
+ if (operation === "runtime_prepare")
135
+ check(context.allowNetwork, "PERMISSION", "Runtime preparation requires dependency-network access");
136
+ check(!(args.flow && args.provider), "SELECT", "Choose flow or provider");
137
+ check(!(args.retry &&
138
+ (args.flow || args.provider || args.runId || args.action !== "run")), "SELECT", "Retry uses the recorded run selection and identity");
139
+ const previous = args.retry ? receipt(root, args.retry) : undefined;
140
+ if (previous) {
141
+ check(previous.action === "run" && previous.environment === environment, "RUN", "Retry requires a data run in the selected environment");
142
+ check(previous.status !== "running", "RUN", "Run is still active");
143
+ }
144
+ check(!previous || !args.from || args.from === previous.from, "SELECT", "Retry uses its recorded build directory");
145
+ const from = previous?.from ?? args.from ?? "build/generated";
146
+ const directory = fence(root, from);
147
+ check(existsSync(resolve(directory, "ingestron-project.json")), "BUILD", "Build the project first; supply from to select a different output directory");
148
+ const checked = validateOutput(directory);
149
+ check(checked.ownership === "managed", "OWNERSHIP", "Execution requires an unchanged managed build");
150
+ const build = JSON.parse(readFileSync(resolve(directory, "ingestron-project.json"), "utf8"));
151
+ check(build.apiVersion === "ingestron.project-build/v1" &&
152
+ build.environment === environment, "BUILD", "Build environment differs; rebuild for this environment");
153
+ for (const [file, hash] of Object.entries(build.sourceFiles ?? {}))
154
+ check(existsSync(fence(root, file)) &&
155
+ digest(readFileSync(fence(root, file))) === hash, "STALE", `Project input changed: ${file}; rebuild before execution`);
156
+ check(build.packageLockSha256 ===
157
+ digest(readFileSync(fence(root, "packages.lock.yaml"))), "STALE", "Package lock changed; rebuild before execution");
158
+ const candidates = build.packages.filter((p) => previous
159
+ ? p.configuration === previous.configuration
160
+ : args.provider
161
+ ? p.configuration === args.provider
162
+ : args.flow
163
+ ? p.scope.flows.includes(args.flow)
164
+ : true);
165
+ check(candidates.length === 1, "SELECT", "Select one execution target with provider or flow");
166
+ const pkg = candidates[0];
167
+ const installed = resolvePackage(root, pkg.reference);
168
+ check(installed.entry.commit === pkg.commit, "STALE", "Execution provider changed; rebuild");
169
+ const manifest = providerPackageSchema.parse(packageYaml(installed.file));
170
+ checkProviderCompatibility(manifest);
171
+ const capability = manifest.execution;
172
+ check(capability, "NOT_IMPLEMENTED", `${pkg.configuration}: run and runtime preparation are not implemented by this provider; build remains available. Cloud deployment requires separate review`);
173
+ check(capability.transport === "local-python/v1", "NOT_IMPLEMENTED", "This execution transport is not implemented by this host");
174
+ const action = operation === "runtime_prepare" ? "prepare" : args.action;
175
+ check(capability.actions.includes(action), "NOT_IMPLEMENTED", "Provider does not support this execution action");
176
+ const flows = previous?.flows ?? (args.flow ? [args.flow] : pkg.scope.flows);
177
+ const fingerprint = digest(canonical({
178
+ plan: checked.planDigest,
179
+ configuration: pkg.configuration,
180
+ flows,
181
+ }));
182
+ if (previous)
183
+ check(previous.fingerprint === fingerprint, "STALE", "Retry requires the same build and flow selection");
184
+ const target = fence(directory, pkg.directory === "."
185
+ ? capability.entryPoint
186
+ : pkg.directory.replace(/\/$/, "") + "/" + capability.entryPoint);
187
+ const packageRoot = pkg.directory === "."
188
+ ? directory
189
+ : fence(directory, pkg.directory.replace(/\/$/, ""));
190
+ const cache = fence(root, ".ingestron/runtimes");
191
+ mkdirSync(cache, { recursive: true });
192
+ const runs = fence(root, ".ingestron/runs");
193
+ mkdirSync(runs, { recursive: true });
194
+ const runId = previous?.id ?? args.runId ?? randomUUID();
195
+ const file = fence(runs, runId + ".json");
196
+ check(previous || !existsSync(file), "RUN", "Run identity already exists; supply retry to repeat it");
197
+ const guard = fence(runs, runId + ".lock");
198
+ if (previous?.status === "indeterminate" && existsSync(guard))
199
+ rmSync(guard, { recursive: true });
200
+ try {
201
+ mkdirSync(guard);
202
+ }
203
+ catch {
204
+ throw new Problem("RUN", "Run is already active");
205
+ }
206
+ const record = {
207
+ apiVersion: "ingestron.run/v1",
208
+ id: runId,
209
+ action,
210
+ status: "running",
211
+ pid: process.pid,
212
+ environment,
213
+ project: build.project,
214
+ provider: pkg.reference,
215
+ providerCommit: pkg.commit,
216
+ from: relative(root, directory),
217
+ configuration: pkg.configuration,
218
+ flows,
219
+ fingerprint,
220
+ attempt: (previous?.attempt ?? 0) + 1,
221
+ startedAt: now(),
222
+ };
223
+ try {
224
+ store(file, record);
225
+ const env = {};
226
+ for (const key of [
227
+ "PATH",
228
+ "HOME",
229
+ "TMPDIR",
230
+ "LANG",
231
+ "SSL_CERT_FILE",
232
+ "HTTPS_PROXY",
233
+ "HTTP_PROXY",
234
+ "NO_PROXY",
235
+ ])
236
+ if (process.env[key])
237
+ env[key] = process.env[key];
238
+ const provided = args.envFile
239
+ ? parseEnv(readFileSync(fence(root, args.envFile), "utf8"))
240
+ : {};
241
+ const allow = new Set();
242
+ const secrets = (value) => {
243
+ if (!value || typeof value !== "object")
244
+ return;
245
+ if (value.$secret?.env)
246
+ allow.add(value.$secret.env);
247
+ for (const v of Object.values(value))
248
+ secrets(v);
249
+ };
250
+ for (const flow of flows) {
251
+ const config = JSON.parse(readFileSync(fence(packageRoot, `flows/${flow}/connector.json`), "utf8"));
252
+ secrets(config.sourceSettings);
253
+ if (config.configEnv)
254
+ allow.add(config.configEnv);
255
+ }
256
+ for (const name of allow) {
257
+ check(/^[A-Za-z_][A-Za-z0-9_]*$/.test(name), "SECRET", "Invalid environment reference");
258
+ check(!/^(PYTHON|PIP|LD_|DYLD_)/.test(name) &&
259
+ !["PATH", "HOME", "TMPDIR"].includes(name), "SECRET", "Secret names cannot override execution environment controls");
260
+ const value = provided[name] ?? process.env[name];
261
+ if (value !== undefined)
262
+ env[name] = value;
263
+ }
264
+ env.PYTHONNOUSERSITE = "1";
265
+ env.PYTHONDONTWRITEBYTECODE = "1";
266
+ const result = await launch("python3", target, {
267
+ apiVersion: "ingestron.execution-request/v1",
268
+ action,
269
+ flows,
270
+ runId,
271
+ cache,
272
+ python: args.python,
273
+ }, packageRoot, env);
274
+ const complete = {
275
+ ...record,
276
+ status: "succeeded",
277
+ finishedAt: now(),
278
+ result,
279
+ };
280
+ store(file, complete);
281
+ return complete;
282
+ }
283
+ catch (error) {
284
+ store(file, {
285
+ ...record,
286
+ status: error instanceof Problem && error.code === "INTERRUPTED"
287
+ ? "interrupted"
288
+ : "failed",
289
+ finishedAt: now(),
290
+ error: "Execution failed; source details withheld",
291
+ });
292
+ throw new Problem(error instanceof Problem ? error.code : "RUNTIME", `${error instanceof Problem ? error.message : "Execution failed"}. Run ${runId}; inspect run_status for this run identity`);
293
+ }
294
+ finally {
295
+ rmSync(guard, { recursive: true, force: true });
296
+ }
297
+ }
@@ -0,0 +1,8 @@
1
+ import { Configuration } from "./config.js";
2
+ import { providerPackageSchema } from "./packages.js";
3
+ import type { Project } from "./schema.js";
4
+ /** Resolve explicit direct packs only. Every input is fingerprinted by the host. */
5
+ export declare function extensionActivities(reader: Configuration, project: Project, names: string[], provider: ReturnType<typeof providerPackageSchema.parse>): Record<string, {
6
+ uses: string;
7
+ with: Record<string, unknown>;
8
+ }>;
@@ -0,0 +1,34 @@
1
+ import { check } from "./errors.js";
2
+ import { extensionPackSchema, packageYaml, resolvePackage, fence, } from "./packages.js";
3
+ /** Resolve explicit direct packs only. Every input is fingerprinted by the host. */
4
+ export function extensionActivities(reader, project, names, provider) {
5
+ const result = {};
6
+ check(new Set(names).size === names.length, "PACK", "Duplicate configured extension pack");
7
+ for (const name of names) {
8
+ const selection = project.providers.packs[name];
9
+ check(selection, "PACK", `Unknown extension pack ${name}`);
10
+ const source = selection.source.startsWith(".")
11
+ ? {
12
+ file: reader.path(fence(reader.root, selection.source.replace(/^\.\//, ""))),
13
+ }
14
+ : resolvePackage(reader.root, `${selection.source}@${selection.version}`);
15
+ const pack = extensionPackSchema.parse(packageYaml(source.file));
16
+ reader.text(source.file);
17
+ if (!selection.source.startsWith("."))
18
+ reader.text("packages.lock.yaml");
19
+ check(selection.version === pack.version ||
20
+ (/^[a-f0-9]{40}$/.test(selection.version) &&
21
+ !selection.source.startsWith(".")), "PACK", `Pack ${name} version differs from selector`);
22
+ check(pack.extends.provider === provider.id &&
23
+ pack.extends.version === provider.version &&
24
+ pack.extends.platform === provider.platform, "PACK", `Pack ${name} requires ${pack.extends.provider}@${pack.extends.version} on ${pack.extends.platform}`);
25
+ check(provider.capabilities?.packContracts[pack.extends.contract]?.version ===
26
+ pack.extends.contractVersion, "PACK", `Pack ${name} requires unsupported extension contract ${pack.extends.contract}@${pack.extends.contractVersion}`);
27
+ for (const [alias, activity] of Object.entries(pack.activities)) {
28
+ check(provider.capabilities.packContracts[pack.extends.contract].activities.includes(activity.uses), "PACK", `Pack ${name} activity ${activity.uses} is outside extension contract ${pack.extends.contract}`);
29
+ check(Object.hasOwn(provider.activities, activity.uses), "PACK", `Pack ${name} references unknown provider activity ${activity.uses}`);
30
+ result[`${name}:${alias}`] = activity;
31
+ }
32
+ }
33
+ return result;
34
+ }
@@ -0,0 +1 @@
1
+ export declare function compilerFingerprint(): string;
@@ -0,0 +1,16 @@
1
+ import { installationRoot } from "./installation.js";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { canonical, digest, check } from "./errors.js";
5
+ export function compilerFingerprint() {
6
+ const root = installationRoot;
7
+ const file = resolve(root, "dist/compiler-fingerprint.json");
8
+ check(existsSync(file), "COMPILER", "Build core before planning (pnpm build)");
9
+ const manifest = JSON.parse(readFileSync(file, "utf8"));
10
+ for (const [name, hash] of Object.entries(manifest.files))
11
+ check(digest(readFileSync(resolve(root, name))) === hash, "COMPILER", "Compiler installation changed; rebuild or reinstall the pinned package");
12
+ if (import.meta.url.endsWith(".ts"))
13
+ for (const [name, hash] of Object.entries(manifest.sources))
14
+ check(digest(readFileSync(resolve(root, name))) === hash, "COMPILER", "Source changed since compilation; run pnpm build before planning");
15
+ return digest(canonical(manifest));
16
+ }
@@ -0,0 +1,16 @@
1
+ import type { Plan } from "./schema.js";
2
+ export type Files = Record<string, string>;
3
+ export declare function verifyPlan(root: string, plan: Plan): void;
4
+ export declare function validateGeneratedFiles(files: Files): void;
5
+ export declare function renderProject(root: string, plan: Plan): Files;
6
+ export declare function writeOutput(directory: string, plan: Pick<Plan, "project" | "environment" | "selection" | "digest">, files: Files, ownership?: "managed" | "team"): {
7
+ directory: string;
8
+ files: number;
9
+ digest: string;
10
+ };
11
+ export declare function validateOutput(directory: string): {
12
+ files: number;
13
+ planDigest: string;
14
+ evidence: string;
15
+ ownership: "managed" | "team";
16
+ };
@@ -0,0 +1,198 @@
1
+ import { generateProvider } from "../plugins/generation.js";
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, lstatSync, realpathSync, renameSync, rmSync, mkdtempSync, } from "node:fs";
3
+ import { resolve, dirname, isAbsolute } from "node:path";
4
+ import { parseDocument } from "yaml";
5
+ import { parser as pythonParser } from "@lezer/python";
6
+ import { check, canonical, digest, Problem } from "./errors.js";
7
+ import { Configuration } from "./config.js";
8
+ import { compilerFingerprint } from "./fingerprint.js";
9
+ import { version } from "../version.js";
10
+ const json = (value) => JSON.stringify(value, null, 2) + "\n";
11
+ export function verifyPlan(root, plan) {
12
+ const { digest: expected, ...body } = plan;
13
+ check(plan.apiVersion === "ingestron.plan/v1" &&
14
+ digest(canonical(body)) === expected, "PLAN", "Plan content digest is invalid");
15
+ check(plan.compilerVersion === version, "STALE", "Compiler version changed; create a new plan");
16
+ for (const [name, hash] of Object.entries(plan.inputDigests))
17
+ check(process.env[name] !== undefined && digest(process.env[name]) === hash, "STALE", `Environment input changed since planning: ${name}`);
18
+ check(plan.compilerDigest === compilerFingerprint(), "STALE", "Compiler content changed since planning; create a new plan");
19
+ const reader = new Configuration(root);
20
+ for (const [file, hash] of Object.entries(plan.files))
21
+ check(digest(reader.text(file)) === hash, "STALE", `Input changed since planning: ${file}`);
22
+ }
23
+ export function validateGeneratedFiles(files) {
24
+ for (const [name, source] of Object.entries(files)) {
25
+ const pythonSources = name.endsWith(".py")
26
+ ? [source]
27
+ : name.endsWith(".ipynb")
28
+ ? JSON.parse(source)
29
+ .cells.filter((cell) => cell.cell_type === "code")
30
+ .map((cell) => Array.isArray(cell.source) ? cell.source.join("") : cell.source)
31
+ : [];
32
+ for (const python of pythonSources) {
33
+ let bad = false;
34
+ pythonParser.parse(python).iterate({
35
+ enter(node) {
36
+ if (node.type.isError)
37
+ bad = true;
38
+ },
39
+ });
40
+ check(!bad, "PYTHON", `Generated Python syntax invalid: ${name}`);
41
+ }
42
+ if (name.endsWith(".json") || name.endsWith(".ipynb"))
43
+ JSON.parse(source);
44
+ if (/\.ya?ml$/.test(name))
45
+ check(!parseDocument(source, { uniqueKeys: true }).errors.length, "YAML", `Generated YAML syntax invalid: ${name}`);
46
+ }
47
+ }
48
+ export function renderProject(root, plan) {
49
+ verifyPlan(root, plan);
50
+ const files = generateProvider(root, plan);
51
+ validateGeneratedFiles(files);
52
+ return files;
53
+ }
54
+ const manifestName = "ingestron-manifest.json";
55
+ function outputPath(root, name) {
56
+ check(!isAbsolute(name) &&
57
+ !name.includes("\\") &&
58
+ name.split("/").every((p) => p && ![".", ".."].includes(p)), "PATH", "Unsafe generated output path");
59
+ let at = root;
60
+ for (const p of name.split("/")) {
61
+ at = resolve(at, p);
62
+ check(!existsSync(at) || !lstatSync(at).isSymbolicLink(), "PATH", "Generated output cannot cross symlinks");
63
+ }
64
+ return at;
65
+ }
66
+ export function writeOutput(directory, plan, files, ownership = "managed") {
67
+ directory = resolve(directory);
68
+ check(!existsSync(directory) || !lstatSync(directory).isSymbolicLink(), "PATH", "Output directory cannot be a symlink");
69
+ mkdirSync(directory, { recursive: true });
70
+ directory = realpathSync(directory);
71
+ const manifestFile = outputPath(directory, manifestName), previous = existsSync(manifestFile)
72
+ ? JSON.parse(readFileSync(manifestFile, "utf8"))
73
+ : undefined;
74
+ check(previous?.ownership !== "team", "OWNER", "This project is team-maintained. Generate into a new directory and review the diff; existing source will not be overwritten.");
75
+ if (ownership === "team") {
76
+ check(!previous && readdirSync(directory).length === 0, "OWNER", "Team export requires a new or empty output directory");
77
+ files = {
78
+ ...files,
79
+ "OWNERSHIP.md": "# Team-maintained project\n\nThe team owns this source and configuration. Ingestron will not regenerate into this directory. Compiler provenance describes the original export, not subsequent edits. No Ingestron runtime is needed. Review changes through Git and run ingestron validate-output on this directory. Maintain business tests, native acceptance and documentation in your own Git workflow.\n",
80
+ };
81
+ }
82
+ if (previous) {
83
+ check(canonical(previous.selection) === canonical(plan.selection), "OWNER", "Use a separate output directory for each selected flow/table/step scope");
84
+ check(previous.apiVersion === "ingestron.output/v1" &&
85
+ previous.project === plan.project &&
86
+ previous.environment === plan.environment, "OWNER", "Output directory belongs to another project/environment");
87
+ for (const [name, hash] of Object.entries(previous.files))
88
+ check(existsSync(outputPath(directory, name)) &&
89
+ digest(readFileSync(outputPath(directory, name))) === hash, "CONFLICT", `Generated output was edited or removed: ${name}`);
90
+ }
91
+ for (const name of Object.keys(files))
92
+ check(!existsSync(outputPath(directory, name)) || !!previous?.files[name], "OWNER", `Refusing to overwrite unowned file ${name}`);
93
+ // Output directories own exactly one generation scope. Remove only unchanged owned files.
94
+ const merged = Object.fromEntries(Object.entries(files).map(([name, text]) => [
95
+ name,
96
+ digest(name.endsWith(".whl") ? Buffer.from(text, "base64") : text),
97
+ ]));
98
+ const manifest = {
99
+ apiVersion: "ingestron.output/v1",
100
+ project: plan.project,
101
+ environment: plan.environment,
102
+ planDigest: plan.digest,
103
+ selection: plan.selection,
104
+ files: merged,
105
+ ownership,
106
+ };
107
+ const guard = outputPath(directory, ".ingestron-generate.lock");
108
+ try {
109
+ writeFileSync(guard, plan.digest, { flag: "wx" });
110
+ }
111
+ catch {
112
+ throw new Problem("CONFLICT", "Another generator owns this output directory");
113
+ }
114
+ const stage = mkdtempSync(resolve(directory, ".generate-")), touched = [], before = new Map();
115
+ try {
116
+ for (const [name, hash] of Object.entries(previous?.files ?? {}))
117
+ check(digest(readFileSync(outputPath(directory, name))) === hash, "CONFLICT", `Output changed while acquiring generation guard: ${name}`);
118
+ for (const name of new Set([
119
+ ...Object.keys(files),
120
+ ...Object.keys(previous?.files ?? {}),
121
+ manifestName,
122
+ ]))
123
+ before.set(name, existsSync(outputPath(directory, name))
124
+ ? readFileSync(outputPath(directory, name))
125
+ : null);
126
+ for (const [name, text] of Object.entries(files)) {
127
+ const staged = outputPath(stage, name);
128
+ mkdirSync(dirname(staged), { recursive: true });
129
+ writeFileSync(staged, name.endsWith(".whl") ? Buffer.from(text, "base64") : text);
130
+ }
131
+ for (const name of Object.keys(files)) {
132
+ const target = outputPath(directory, name);
133
+ mkdirSync(dirname(target), { recursive: true });
134
+ renameSync(outputPath(stage, name), target);
135
+ touched.push(name);
136
+ }
137
+ for (const name of Object.keys(previous?.files ?? {}))
138
+ if (!Object.hasOwn(files, name)) {
139
+ rmSync(outputPath(directory, name));
140
+ touched.push(name);
141
+ }
142
+ const stagedManifest = resolve(stage, manifestName);
143
+ writeFileSync(stagedManifest, json(manifest));
144
+ renameSync(stagedManifest, manifestFile);
145
+ touched.push(manifestName);
146
+ }
147
+ catch (error) {
148
+ for (const name of touched.reverse()) {
149
+ const file = outputPath(directory, name), bytes = before.get(name);
150
+ if (bytes == null)
151
+ rmSync(file, { force: true });
152
+ else
153
+ writeFileSync(file, bytes);
154
+ }
155
+ throw error;
156
+ }
157
+ finally {
158
+ rmSync(stage, { recursive: true, force: true });
159
+ rmSync(guard, { force: true });
160
+ }
161
+ return { directory, files: Object.keys(files).length, digest: plan.digest };
162
+ }
163
+ export function validateOutput(directory) {
164
+ directory = realpathSync(directory);
165
+ const manifest = JSON.parse(readFileSync(outputPath(directory, manifestName), "utf8"));
166
+ check(manifest.apiVersion === "ingestron.output/v1", "OUTPUT", "Unknown output manifest version");
167
+ const files = {};
168
+ const inventory = manifest.ownership === "team"
169
+ ? Object.fromEntries(teamFiles(directory).map((name) => [name, ""]))
170
+ : manifest.files;
171
+ for (const [name, hash] of Object.entries(inventory)) {
172
+ const bytes = readFileSync(outputPath(directory, name));
173
+ const content = bytes.toString(name.endsWith(".whl") ? "base64" : "utf8");
174
+ check(manifest.ownership === "team" || digest(bytes) === hash, "CONFLICT", `Output hash mismatch: ${name}`);
175
+ files[name] = content;
176
+ }
177
+ validateGeneratedFiles(files);
178
+ return {
179
+ files: Object.keys(files).length,
180
+ planDigest: manifest.planDigest,
181
+ evidence: "offline",
182
+ ownership: manifest.ownership ?? "managed",
183
+ };
184
+ }
185
+ function teamFiles(root, prefix = "") {
186
+ return readdirSync(resolve(root, prefix), { withFileTypes: true }).flatMap((entry) => {
187
+ if ([".git", ".venv", "node_modules", "__pycache__", ".ingestron"].includes(entry.name))
188
+ return [];
189
+ const name = prefix ? prefix + "/" + entry.name : entry.name;
190
+ check(!entry.isSymbolicLink(), "PATH", "Team source cannot contain symlinks");
191
+ if (entry.isDirectory())
192
+ return teamFiles(root, name);
193
+ return /\.(py|sql|json|ipynb|ya?ml|whl)$/.test(name) &&
194
+ name !== manifestName
195
+ ? [name]
196
+ : [];
197
+ });
198
+ }
@@ -0,0 +1,3 @@
1
+ export declare const installationRoot: string;
2
+ export declare const assetPath: (path: string) => string;
3
+ export declare const compiledPath: (path: string) => string;
@@ -0,0 +1,14 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ let directory = dirname(fileURLToPath(import.meta.url));
5
+ while (!existsSync(join(directory, "package.json")) ||
6
+ !["@ingestron/core"].includes(JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).name)) {
7
+ const parent = dirname(directory);
8
+ if (parent === directory)
9
+ throw new Error("Cannot locate the Ingestron core installation");
10
+ directory = parent;
11
+ }
12
+ export const installationRoot = directory;
13
+ export const assetPath = (path) => join(installationRoot, "dist/assets", path);
14
+ export const compiledPath = (path) => join(installationRoot, "dist", path);
@@ -0,0 +1,41 @@
1
+ export type PluginKind = "provider" | "connector" | "model-pack" | "activity-pack" | "report-pack";
2
+ export declare const pluginKindLabel: (kind: PluginKind) => string;
3
+ export declare function browseProviders(root: string, query?: string, kind?: string, filters?: {
4
+ engine?: string;
5
+ installable?: boolean;
6
+ }): {
7
+ id: string;
8
+ name: string;
9
+ version: string;
10
+ kind: PluginKind;
11
+ kindLabel: string;
12
+ reference: string;
13
+ commit: string;
14
+ repository: string;
15
+ description: string;
16
+ engines: string[];
17
+ availability: string;
18
+ installable: boolean;
19
+ documentation: string;
20
+ licence: string;
21
+ url: string;
22
+ evidence: string;
23
+ }[];
24
+ export declare function pluginDetails(root: string, id: string): {
25
+ id: string;
26
+ name: string;
27
+ version: string;
28
+ kind: PluginKind;
29
+ kindLabel: string;
30
+ reference: string;
31
+ commit: string;
32
+ repository: string;
33
+ description: string;
34
+ engines: string[];
35
+ availability: string;
36
+ installable: boolean;
37
+ documentation: string;
38
+ licence: string;
39
+ url: string;
40
+ evidence: string;
41
+ };