@unoverse-platform/studio 0.1.97 → 0.1.99
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/bin/studio.mjs +2 -2
- package/local/catalog.ts +5 -1
- package/local/nodes.ts +28 -41
- package/local/plugin.ts +71 -13
- package/local/sample-identity/brand.yaml +24 -0
- package/local/sample-identity/organisation.yaml +14 -0
- package/local/sample-identity/purpose.yaml +14 -0
- package/local/sample-identity/story.yaml +18 -0
- package/local/scaffold.mjs +43 -23
- package/local/source.ts +3 -8
- package/package.json +1 -1
- package/screens/AgentsView.tsx +173 -0
- package/screens/ComponentsView.tsx +2 -2
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/IdentityView.tsx +3 -3
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +2 -0
- package/src/App.tsx +8 -3
- package/src/host.ts +1 -14
- package/vendor/base/src/agent-mcp/discovery.ts +76 -18
- package/vendor/base/src/agent-mcp/handoff.ts +11 -31
- package/vendor/base/src/agent-mcp/index.ts +5 -5
- package/vendor/base/src/agent-mcp/invoke.ts +37 -19
- package/vendor/base/src/agent-mcp/lean.ts +19 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
- package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
- package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
- package/vendor/base/src/agent-mcp/tools.ts +136 -7
- package/vendor/base/src/agent-mcp/types.ts +47 -10
- package/vendor/base/src/auth/auth.ts +35 -5
- package/vendor/base/src/definitions/isolated.ts +37 -0
- package/vendor/base/src/definitions/load/compile/component.ts +59 -4
- package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
- package/vendor/base/src/definitions/load/compile/template.ts +19 -15
- package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
- package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
- package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
- package/vendor/base/src/definitions/load/load.ts +37 -28
- package/vendor/base/src/definitions/model.ts +8 -12
- package/vendor/base/src/definitions/registry/agents.ts +117 -0
- package/vendor/base/src/definitions/registry/catalog.ts +40 -24
- package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
- package/vendor/base/src/definitions/registry/homes.ts +69 -5
- package/vendor/base/src/definitions/registry/objects.ts +413 -0
- package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
- package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
- package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
- package/vendor/base/src/definitions/theme.ts +1 -2
- package/vendor/base/src/items/collect.ts +29 -25
- package/vendor/base/src/items/publish.ts +30 -6
- package/vendor/base/src/lint/context.mjs +17 -0
- package/vendor/base/src/lint/design/file.mjs +199 -2
- package/vendor/base/src/lint/design/index.mjs +66 -73
- package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
- package/vendor/base/src/lint/design/walk.mjs +14 -4
- package/vendor/base/src/lint/index.mjs +15 -9
- package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
- package/vendor/base/src/lint/nodes/index.mjs +5 -0
- package/vendor/base/src/lint/nodes/node.mjs +157 -7
- package/vendor/base/src/lint/objects/index.d.mts +32 -0
- package/vendor/base/src/lint/objects/index.mjs +324 -0
- package/vendor/base/src/lint/objects/rules.d.mts +44 -0
- package/vendor/base/src/lint/objects/rules.mjs +446 -0
- package/vendor/base/src/lint/prompts/index.d.mts +34 -0
- package/vendor/base/src/lint/prompts/index.mjs +198 -0
- package/vendor/base/src/manifests/compose.ts +80 -9
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +9 -1
- package/vendor/base/src/manifests/executor/memory.ts +83 -3
- package/vendor/base/src/manifests/executor/session.ts +50 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
- package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
- package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
- package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
- package/vendor/base/src/manifests/runtime/context.ts +18 -6
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
- package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
- package/vendor/base/src/manifests/runtime/events.ts +39 -3
- package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
- package/vendor/base/src/manifests/runtime/index.ts +3 -2
- package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
- package/vendor/base/src/manifests/runtime/usage.ts +10 -0
- package/vendor/base/src/paths.ts +16 -8
- package/vendor/base/src/platform/audioManager.ts +32 -0
- package/vendor/base/src/platform/executionContext.ts +14 -0
- package/vendor/base/src/platform/liveInput.ts +52 -0
- package/vendor/base/src/plugins/discovery.ts +1 -1
- package/vendor/base/src/template/SafeExpression.ts +6 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
- package/vendor/sdk/appEngine.tsx +30 -11
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
- package/vendor/sdk/lib/app.tsx +70 -95
- package/vendor/sdk/lib/connection.tsx +47 -57
- package/vendor/sdk/lib/core/actions.ts +6 -0
- package/vendor/sdk/lib/core/apps.ts +66 -81
- package/vendor/sdk/lib/core/client.ts +5 -14
- package/vendor/sdk/lib/core/connection.ts +30 -0
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +90 -15
- package/vendor/sdk/lib/core/types.ts +9 -8
- package/vendor/sdk/lib/primitives.tsx +113 -4
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
- package/vendor/sdk/lib/render.tsx +3 -3
- package/vendor/sdk/lib/voice.tsx +27 -13
- package/local/sample-identity/brand/brand.yaml +0 -88
- package/local/sample-identity/brand/manifest.yaml +0 -13
- package/local/sample-identity/organisation/manifest.yaml +0 -15
- package/local/sample-identity/organisation/organisation.yaml +0 -53
- package/local/sample-identity/purpose/manifest.yaml +0 -12
- package/local/sample-identity/purpose/purpose.yaml +0 -46
- package/local/sample-identity/story/manifest.yaml +0 -12
- package/local/sample-identity/story/story.yaml +0 -63
- package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the objects and pipelines linter, which is plain JavaScript.
|
|
3
|
+
*
|
|
4
|
+
* Same arrangement as ../design/index.d.mts and ../nodes/index.d.mts: the linter stays
|
|
5
|
+
* .mjs so the CLIs run it under bare node, and TypeScript consumers (the publish gate,
|
|
6
|
+
* Studio) get a shape rather than `any`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Finding {
|
|
10
|
+
level: "error" | "warn" | "hint";
|
|
11
|
+
file: string;
|
|
12
|
+
line?: number;
|
|
13
|
+
msg: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ObjectsResult {
|
|
17
|
+
problems: Finding[];
|
|
18
|
+
objectCount: number;
|
|
19
|
+
pipelineCount: number;
|
|
20
|
+
designRoot: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LintObjectsOptions {
|
|
24
|
+
/** Absolute path -> text that stands in for what is on disk (Studio's unsaved editor). */
|
|
25
|
+
overlay?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Lint every object and pipeline folder under `designRoot`, every tier. Prints nothing, throws nothing for a lint failure. */
|
|
29
|
+
export function lintObjects(designRoot: string, options?: LintObjectsOptions): ObjectsResult;
|
|
30
|
+
|
|
31
|
+
/** True when anything would fail a build. */
|
|
32
|
+
export function hasErrors(result: ObjectsResult): boolean;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The objects and pipelines linter, as a LIBRARY. Rules in, findings out.
|
|
3
|
+
*
|
|
4
|
+
* A THIRD SIBLING of ../design/ and ../nodes/ (docs/unoverse/data/objects/README.md 9a,
|
|
5
|
+
* 2026-09-10). An object and a pipeline are folders in the node grammar, so they are linted
|
|
6
|
+
* the way nodes are: tier 2, every file against a schema (packages/docs/schemas/objects and
|
|
7
|
+
* /pipelines); tier 3, the rules a schema cannot say (rules.mjs, the SAME functions the
|
|
8
|
+
* loaders refuse a file with). Not the design linter's way: there is no primitive tree here.
|
|
9
|
+
*
|
|
10
|
+
* Callers: `unoverse lint` and `POST /publish` through items/publish lintForPublish; Studio's
|
|
11
|
+
* save and draft lint; CI. Prints nothing, exits nothing, throws nothing for a lint failure.
|
|
12
|
+
*
|
|
13
|
+
* `options.overlay` maps an absolute path to text that STANDS IN for what is on disk, so
|
|
14
|
+
* Studio's editor lints what the developer has typed before it is saved, through these
|
|
15
|
+
* rules and no others.
|
|
16
|
+
*/
|
|
17
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
18
|
+
import { join, resolve, dirname, relative, basename } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
import { problems, reset, report, state } from "../context.mjs";
|
|
22
|
+
import { validateAgainst, Validator } from "../nodes/schema.mjs";
|
|
23
|
+
import {
|
|
24
|
+
envelopeFindings, propsFindings, fromFindings, serviceFindings, makeFindings, rowKeyFindings, objectTestFindings,
|
|
25
|
+
pipelineFindings, pipelineFolderFindings,
|
|
26
|
+
} from "./rules.mjs";
|
|
27
|
+
|
|
28
|
+
const require_ = createRequire(import.meta.url);
|
|
29
|
+
let YAML;
|
|
30
|
+
try {
|
|
31
|
+
YAML = require_("yaml");
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error("the objects linter needs yaml (npm install -D yaml)");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
|
|
38
|
+
export const SCHEMA_ID = {
|
|
39
|
+
object: "https://docs.unoverse.ai/schemas/objects/object.schema.json",
|
|
40
|
+
props: "https://docs.unoverse.ai/schemas/objects/props.schema.json",
|
|
41
|
+
service: "https://docs.unoverse.ai/schemas/objects/service.schema.json",
|
|
42
|
+
make: "https://docs.unoverse.ai/schemas/objects/make.schema.json",
|
|
43
|
+
objectTest: "https://docs.unoverse.ai/schemas/objects/test.schema.json",
|
|
44
|
+
dataset: "https://docs.unoverse.ai/schemas/objects/dataset.schema.json",
|
|
45
|
+
pipeline: "https://docs.unoverse.ai/schemas/pipelines/pipeline.schema.json",
|
|
46
|
+
config: "https://docs.unoverse.ai/schemas/pipelines/config.schema.json",
|
|
47
|
+
interface: "https://docs.unoverse.ai/schemas/pipelines/interface.schema.json",
|
|
48
|
+
pipelineTest: "https://docs.unoverse.ai/schemas/pipelines/test.schema.json",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Where the schemas are, for this run. Their home is packages/docs/schemas/{objects,
|
|
53
|
+
* pipelines,nodes}, served by the docs site; in dist they sit beside this file
|
|
54
|
+
* (copy-lint.mjs puts them there, the CLI vendors that folder). The node schemas ride too:
|
|
55
|
+
* a pipeline's config, interface and test files ARE a node's, and the object schemas
|
|
56
|
+
* reach into _defs for an expression.
|
|
57
|
+
*/
|
|
58
|
+
function schemaDirs() {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const kind of ["objects", "pipelines", "nodes"]) {
|
|
61
|
+
for (const c of [join(HERE, "_schema", kind), resolve(HERE, `../../../../docs/schemas/${kind}`), join(HERE, "../nodes/_schema")]) {
|
|
62
|
+
if (existsSync(c)) {
|
|
63
|
+
out.push(c);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadAllSchemas() {
|
|
72
|
+
for (const dir of schemaDirs())
|
|
73
|
+
for (const f of readdirSync(dir).filter((f) => f.endsWith(".json")))
|
|
74
|
+
try {
|
|
75
|
+
const s = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
76
|
+
if (s.$id && !state.schemas[s.$id]) state.schemas[s.$id] = s;
|
|
77
|
+
} catch (e) {
|
|
78
|
+
report("error", join(dir, f), `is not valid JSON: ${e.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isDir = (p) => existsSync(p) && statSync(p).isDirectory();
|
|
83
|
+
const isRecord = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Lint every object and pipeline folder under `designRoot`: the marketplace tier and every
|
|
87
|
+
* org's. Returns `{ problems, objectCount, pipelineCount, designRoot }`.
|
|
88
|
+
*/
|
|
89
|
+
export function lintObjects(designRoot, options = {}) {
|
|
90
|
+
const ROOT = resolve(designRoot);
|
|
91
|
+
reset(ROOT, "");
|
|
92
|
+
const overlay = {};
|
|
93
|
+
for (const [k, v] of Object.entries(options.overlay ?? {})) overlay[resolve(k)] = v;
|
|
94
|
+
const readText = (f) => (overlay[resolve(f)] === undefined ? readFileSync(f, "utf8") : overlay[resolve(f)]);
|
|
95
|
+
const rel = (f) => relative(process.cwd(), f).startsWith("..") ? f : relative(process.cwd(), f);
|
|
96
|
+
|
|
97
|
+
const readYaml = (file) => {
|
|
98
|
+
if (!existsSync(file) && overlay[resolve(file)] === undefined) return null;
|
|
99
|
+
try {
|
|
100
|
+
const doc = YAML.parse(readText(file));
|
|
101
|
+
if (doc && typeof doc === "object") delete doc.$schema;
|
|
102
|
+
return doc ?? {};
|
|
103
|
+
} catch (e) {
|
|
104
|
+
report("error", rel(file), `is not valid YAML: ${e.message.split("\n")[0]}`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (!isDir(ROOT)) {
|
|
110
|
+
report("error", ROOT, "no design/ folder here");
|
|
111
|
+
return { problems: [...problems], objectCount: 0, pipelineCount: 0, designRoot: ROOT };
|
|
112
|
+
}
|
|
113
|
+
if (!Validator) {
|
|
114
|
+
report("error", ROOT, "the objects linter needs @cfworker/json-schema");
|
|
115
|
+
return { problems: [...problems], objectCount: 0, pipelineCount: 0, designRoot: ROOT };
|
|
116
|
+
}
|
|
117
|
+
loadAllSchemas();
|
|
118
|
+
|
|
119
|
+
// ── the tiers: the marketplace, then every org folder ──
|
|
120
|
+
const marketplace = join(ROOT, "marketplace");
|
|
121
|
+
const tiers = [];
|
|
122
|
+
if (isDir(marketplace)) tiers.push({ org: undefined, dir: marketplace });
|
|
123
|
+
for (const e of readdirSync(ROOT)) {
|
|
124
|
+
if (e.startsWith(".") || e === "marketplace" || e === "_schema") continue;
|
|
125
|
+
const d = join(ROOT, e);
|
|
126
|
+
if (isDir(d)) tiers.push({ org: e, dir: d });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* WHAT A TIER CAN SEE. A node named by a call or a collect step must be one the org
|
|
131
|
+
* has: the marketplace's, or its own under design/<org>/nodes/. A developer's workspace
|
|
132
|
+
* carries no marketplace on disk (it is installed, never baked), so when the marketplace
|
|
133
|
+
* tier is absent the existence rules step back to a hint rather than reporting every
|
|
134
|
+
* shipped node as unknown: a rule that cannot see must not read that as clean, nor as
|
|
135
|
+
* wrong.
|
|
136
|
+
*/
|
|
137
|
+
const nodeTypesIn = (tierDir) => {
|
|
138
|
+
const out = new Set();
|
|
139
|
+
const home = join(tierDir, "nodes");
|
|
140
|
+
if (!isDir(home)) return out;
|
|
141
|
+
for (const pkg of readdirSync(home)) {
|
|
142
|
+
const nodes = join(home, pkg, "nodes");
|
|
143
|
+
if (!isDir(nodes)) continue;
|
|
144
|
+
for (const n of readdirSync(nodes)) {
|
|
145
|
+
const nodeYaml = join(nodes, n, "node.yaml");
|
|
146
|
+
if (!existsSync(nodeYaml)) continue;
|
|
147
|
+
const m = /^type:\s*([A-Za-z0-9_]+)/m.exec(readFileSync(nodeYaml, "utf8"));
|
|
148
|
+
out.add(m ? m[1] : n);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
};
|
|
153
|
+
const foldersIn = (tierDir, kind) => {
|
|
154
|
+
const home = join(tierDir, kind);
|
|
155
|
+
if (!isDir(home)) return [];
|
|
156
|
+
return readdirSync(home).filter((e) => !e.startsWith(".") && !e.startsWith("_") && isDir(join(home, e)));
|
|
157
|
+
};
|
|
158
|
+
const marketplaceSeen = tiers.some((t) => t.org === undefined);
|
|
159
|
+
const marketplaceNodes = marketplaceSeen ? nodeTypesIn(marketplace) : null;
|
|
160
|
+
const marketplaceObjects = new Set(marketplaceSeen ? foldersIn(marketplace, "objects") : []);
|
|
161
|
+
const marketplacePipelines = new Set(marketplaceSeen ? foldersIn(marketplace, "pipelines") : []);
|
|
162
|
+
const visible = (tier, kind) => {
|
|
163
|
+
const own = new Set(foldersIn(tier.dir, kind));
|
|
164
|
+
if (!marketplaceSeen) return null;
|
|
165
|
+
for (const n of kind === "objects" ? marketplaceObjects : marketplacePipelines) own.add(n);
|
|
166
|
+
return own;
|
|
167
|
+
};
|
|
168
|
+
const visibleNodes = (tier) => {
|
|
169
|
+
if (!marketplaceSeen) return null;
|
|
170
|
+
const s = new Set(marketplaceNodes);
|
|
171
|
+
if (tier.org) for (const n of nodeTypesIn(tier.dir)) s.add(n);
|
|
172
|
+
return s;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// ── objects ──
|
|
176
|
+
// An object's folder, read raw: envelope, own props, service, test. The lint composes
|
|
177
|
+
// `extends` itself (across tiers, the org's first) so the row keys can be checked against
|
|
178
|
+
// every field the object carries, the base's included.
|
|
179
|
+
const objectFolder = (dir, name) => {
|
|
180
|
+
const envFile = [`${name}.yaml`, `${name}.json`].map((f) => join(dir, f)).find((f) => existsSync(f) || overlay[resolve(f)] !== undefined);
|
|
181
|
+
if (!envFile) return null;
|
|
182
|
+
const envelope = readYaml(envFile);
|
|
183
|
+
let props = isRecord(envelope) ? envelope.props : undefined;
|
|
184
|
+
let propsFile = envFile;
|
|
185
|
+
if (isRecord(props) && typeof props.$include === "string") {
|
|
186
|
+
propsFile = join(dir, `${props.$include}.yaml`);
|
|
187
|
+
props = readYaml(propsFile);
|
|
188
|
+
if (props === null && !existsSync(propsFile)) report("error", rel(envFile), `object ${name}: props $include "${envelope.props.$include}" resolves to no file in the folder`);
|
|
189
|
+
}
|
|
190
|
+
const servicePath = join(dir, "api", "service.yaml");
|
|
191
|
+
const service = existsSync(servicePath) || overlay[resolve(servicePath)] !== undefined ? readYaml(servicePath) : undefined;
|
|
192
|
+
// THE PRIVATE MAKE FUNCTION (objects README 4b, 2026-09-14): api/make.yaml, one function, `make`.
|
|
193
|
+
const makePath = join(dir, "api", "make.yaml");
|
|
194
|
+
const make = existsSync(makePath) || overlay[resolve(makePath)] !== undefined ? readYaml(makePath) : undefined;
|
|
195
|
+
const testPath = join(dir, "test.yaml");
|
|
196
|
+
const test = existsSync(testPath) || overlay[resolve(testPath)] !== undefined ? readYaml(testPath) : undefined;
|
|
197
|
+
// THE TABLE IT OWNS (objects README 4f, dataset.md): dataset.yaml, never table.yaml, which
|
|
198
|
+
// would be the envelope of an object NAMED table.
|
|
199
|
+
const datasetPath = join(dir, "dataset.yaml");
|
|
200
|
+
const dataset = datasetPath !== envFile && (existsSync(datasetPath) || overlay[resolve(datasetPath)] !== undefined) ? readYaml(datasetPath) : undefined;
|
|
201
|
+
return { dir, name, envFile, envelope, props, propsFile, service, servicePath, make, makePath, test, testPath, dataset, datasetPath };
|
|
202
|
+
};
|
|
203
|
+
const objectIndex = new Map(); // `${org ?? ""}/${name}` -> folder
|
|
204
|
+
const datasetTables = new Map(); // the table a dataset would own -> the object that owns it
|
|
205
|
+
for (const tier of tiers) for (const name of foldersIn(tier.dir, "objects")) {
|
|
206
|
+
const f = objectFolder(join(tier.dir, "objects", name), name);
|
|
207
|
+
if (f) objectIndex.set(`${tier.org ?? ""}/${name}`, { ...f, org: tier.org });
|
|
208
|
+
}
|
|
209
|
+
const findObject = (name, org) => objectIndex.get(`${org ?? ""}/${name}`) ?? objectIndex.get(`/${name}`) ?? null;
|
|
210
|
+
/** The composed props of an object, or null on a cycle or a missing base (reported by the caller). */
|
|
211
|
+
const composedProps = (folder, chain = []) => {
|
|
212
|
+
const own = isRecord(folder.props) ? folder.props : {};
|
|
213
|
+
const base = isRecord(folder.envelope) ? folder.envelope.extends : undefined;
|
|
214
|
+
if (typeof base !== "string") return { ...own };
|
|
215
|
+
if (chain.includes(folder.name) || chain.includes(base)) return null;
|
|
216
|
+
const parent = findObject(base, folder.org);
|
|
217
|
+
if (!parent) return null;
|
|
218
|
+
const pp = composedProps(parent, [...chain, folder.name]);
|
|
219
|
+
return pp ? { ...pp, ...own } : null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** An object's own functions by name: api/service.yaml's, and `make` from api/make.yaml. */
|
|
223
|
+
const ownFunctions = (folder) => ({ ...(isRecord(folder.service) ? folder.service : {}), ...(folder.make != null ? { make: folder.make } : {}) });
|
|
224
|
+
/** The functions an object can call: its own over every base's (objects README 5, "Inheritance is whole"). */
|
|
225
|
+
const composedService = (folder, chain = []) => {
|
|
226
|
+
const own = ownFunctions(folder);
|
|
227
|
+
const base = isRecord(folder.envelope) ? folder.envelope.extends : undefined;
|
|
228
|
+
if (typeof base !== "string" || chain.includes(folder.name) || chain.includes(base)) return { ...own };
|
|
229
|
+
const parent = findObject(base, folder.org);
|
|
230
|
+
return parent ? { ...composedService(parent, [...chain, folder.name]), ...own } : { ...own };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
let objectCount = 0;
|
|
234
|
+
for (const [, f] of objectIndex) {
|
|
235
|
+
objectCount++;
|
|
236
|
+
const tier = { org: f.org, dir: f.org ? join(ROOT, f.org) : marketplace };
|
|
237
|
+
const at = (file) => rel(join(f.dir, file));
|
|
238
|
+
if (f.envelope === null) continue;
|
|
239
|
+
validateAgainst(SCHEMA_ID.object, f.envelope, rel(f.envFile), `object ${f.name}: the envelope`);
|
|
240
|
+
if (f.props != null && f.propsFile !== f.envFile) validateAgainst(SCHEMA_ID.props, f.props, rel(f.propsFile), `object ${f.name}: the props`);
|
|
241
|
+
if (f.service != null) validateAgainst(SCHEMA_ID.service, f.service, rel(f.servicePath), `object ${f.name}: the functions`);
|
|
242
|
+
if (f.make != null) validateAgainst(SCHEMA_ID.make, f.make, rel(f.makePath), `object ${f.name}: the make function`);
|
|
243
|
+
if (f.test != null) validateAgainst(SCHEMA_ID.objectTest, f.test, rel(f.testPath), `object ${f.name}: the test`);
|
|
244
|
+
if (f.dataset != null) validateAgainst(SCHEMA_ID.dataset, f.dataset, rel(f.datasetPath), `object ${f.name}: the table it owns`);
|
|
245
|
+
// TWO OBJECTS MAY NOT ARRIVE AT ONE TABLE. A dataset's table is named from its object and its
|
|
246
|
+
// org, squeezed to letters, digits and underscores, so "my-thing" and "my_thing" would be one
|
|
247
|
+
// table and the second object would quietly write into the first's rows.
|
|
248
|
+
if (f.dataset != null) {
|
|
249
|
+
const table = `dataset_${[f.org, f.name].filter(Boolean).map((p) => String(p).replace(/[^a-z0-9]+/gi, "_").toLowerCase()).join("_")}`;
|
|
250
|
+
const taken = datasetTables.get(table);
|
|
251
|
+
if (taken && taken !== `${f.org ?? ""}/${f.name}`) report("error", rel(f.datasetPath), `object ${f.name}: its table would be "${table}", which "${taken}" already owns. Two objects cannot share a table: rename one.`);
|
|
252
|
+
else datasetTables.set(table, `${f.org ?? ""}/${f.name}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const say = (list) => {
|
|
256
|
+
for (const x of list) report(x.level ?? "error", at(x.file), x.msg);
|
|
257
|
+
};
|
|
258
|
+
say(envelopeFindings(f.name, f.envelope, basename(f.envFile)));
|
|
259
|
+
|
|
260
|
+
// extends: resolves, no cycle, across the tiers the org can see.
|
|
261
|
+
let parentProps = null;
|
|
262
|
+
const base = isRecord(f.envelope) ? f.envelope.extends : undefined;
|
|
263
|
+
if (typeof base === "string") {
|
|
264
|
+
const parent = findObject(base, f.org);
|
|
265
|
+
if (!parent) report("error", rel(f.envFile), `object ${f.name}: extends "${base}", which no tier holds`);
|
|
266
|
+
else {
|
|
267
|
+
parentProps = composedProps(parent, [f.name]);
|
|
268
|
+
if (parentProps === null) report("error", rel(f.envFile), `object ${f.name}: extends cycle through ${base}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
say(propsFindings(f.name, f.props, parentProps, base, basename(f.propsFile)));
|
|
272
|
+
if (isRecord(f.props)) for (const [k, p] of Object.entries(f.props)) if (isRecord(p) && p.from != null) say(fromFindings(f.name, k, p.from, composedService(f), basename(f.propsFile)));
|
|
273
|
+
say(serviceFindings(f.name, f.service, visibleNodes(tier)));
|
|
274
|
+
say(makeFindings(f.name, f.make, f.service, visibleNodes(tier)));
|
|
275
|
+
if (!marketplaceSeen && isRecord(f.service)) report("hint", rel(f.servicePath), `object ${f.name}: the marketplace is not on disk here, so the nodes its calls name were not checked`);
|
|
276
|
+
const all = composedProps(f) ?? (isRecord(f.props) ? f.props : {});
|
|
277
|
+
say(rowKeyFindings(f.name, f.envelope, all, basename(f.envFile)));
|
|
278
|
+
say(objectTestFindings(f.name, f.test, ownFunctions(f)));
|
|
279
|
+
// An org may never take a marketplace name: a bare object_type must stay unambiguous.
|
|
280
|
+
if (f.org && marketplaceObjects.has(f.name)) report("error", rel(f.envFile), `object ${f.name}: an org's object may not take a marketplace object's name; the row's object_type would be ambiguous. Rename it, or extend ${f.name} under a name of its own`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── pipelines ──
|
|
284
|
+
let pipelineCount = 0;
|
|
285
|
+
for (const tier of tiers) for (const name of foldersIn(tier.dir, "pipelines")) {
|
|
286
|
+
pipelineCount++;
|
|
287
|
+
const dir = join(tier.dir, "pipelines", name);
|
|
288
|
+
const at = (file) => rel(join(dir, file));
|
|
289
|
+
const filePath = join(dir, "pipeline.yaml");
|
|
290
|
+
const file = readYaml(filePath);
|
|
291
|
+
if (file === null) {
|
|
292
|
+
if (!existsSync(filePath)) report("error", at("pipeline.yaml"), `pipeline ${name}: the folder has no pipeline.yaml`);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const configPath = join(dir, "config.yaml");
|
|
296
|
+
const config = existsSync(configPath) || overlay[resolve(configPath)] !== undefined ? readYaml(configPath) : undefined;
|
|
297
|
+
const ifacePath = join(dir, "interface.yaml");
|
|
298
|
+
const iface = existsSync(ifacePath) || overlay[resolve(ifacePath)] !== undefined ? readYaml(ifacePath) : undefined;
|
|
299
|
+
const testPath = join(dir, "test.yaml");
|
|
300
|
+
const test = existsSync(testPath) || overlay[resolve(testPath)] !== undefined ? readYaml(testPath) : undefined;
|
|
301
|
+
|
|
302
|
+
validateAgainst(SCHEMA_ID.pipeline, file, at("pipeline.yaml"), `pipeline ${name}`);
|
|
303
|
+
if (config != null) validateAgainst(SCHEMA_ID.config, config, at("config.yaml"), `pipeline ${name}: the Import form`);
|
|
304
|
+
if (iface != null) validateAgainst(SCHEMA_ID.interface, iface, at("interface.yaml"), `pipeline ${name}: the interface`);
|
|
305
|
+
if (test != null) validateAgainst(SCHEMA_ID.pipelineTest, test, at("test.yaml"), `pipeline ${name}: the test`);
|
|
306
|
+
|
|
307
|
+
const say = (list) => {
|
|
308
|
+
for (const x of list) report(x.level ?? "error", at(x.file), x.msg);
|
|
309
|
+
};
|
|
310
|
+
say(pipelineFindings(name, file));
|
|
311
|
+
say(pipelineFolderFindings(name, { file, config: config ?? null, test }, { nodeTypes: visibleNodes(tier), objectNames: visible(tier, "objects") }));
|
|
312
|
+
if (!marketplaceSeen) report("hint", at("pipeline.yaml"), `pipeline ${name}: the marketplace is not on disk here, so the nodes its steps name and the objects it makes were not checked`);
|
|
313
|
+
if (tier.org && marketplacePipelines.has(name)) report("error", at("pipeline.yaml"), `pipeline ${name}: an org's pipeline may not take a marketplace pipeline's name; the connector's name would be ambiguous`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const rank = { error: 0, warn: 1, hint: 2 };
|
|
317
|
+
const sorted = [...problems].sort((a, b) => rank[a.level] - rank[b.level] || a.file.localeCompare(b.file));
|
|
318
|
+
return { problems: sorted, objectCount, pipelineCount, designRoot: ROOT };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** True when anything would fail a build. Warnings and hints inform, errors stop. */
|
|
322
|
+
export function hasErrors(result) {
|
|
323
|
+
return result.problems.some((p) => p.level === "error");
|
|
324
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for rules.mjs: the pure rules an object or a pipeline file answers to, shared by
|
|
3
|
+
* the linter and the loaders in definitions/registry (one spelling of every rule).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface RuleFinding {
|
|
7
|
+
/** The folder-relative file the mistake is in (`deal.yaml`, `props.yaml`, `api/service.yaml`, `pipeline.yaml`). */
|
|
8
|
+
file: string;
|
|
9
|
+
msg: string;
|
|
10
|
+
/** Absent means error. */
|
|
11
|
+
level?: "error" | "warn" | "hint";
|
|
12
|
+
/** A loader cannot read past this one: the grammar itself. The rest is the linter's to report. */
|
|
13
|
+
refuses?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const KEBAB: RegExp;
|
|
17
|
+
export const ELEMENTS: Set<string>;
|
|
18
|
+
export const FACE_KEYS: string[];
|
|
19
|
+
export const PROP_KEYS: Set<string>;
|
|
20
|
+
export const LIMIT_KEYS: string[];
|
|
21
|
+
|
|
22
|
+
export function envelopeFindings(name: string, envelope: unknown, file?: string): RuleFinding[];
|
|
23
|
+
export function propsFindings(name: string, ownProps: unknown, parentProps?: Record<string, unknown> | null, base?: string | null, file?: string): RuleFinding[];
|
|
24
|
+
export function fromFindings(name: string, prop: string, from: unknown, service: unknown, file?: string): RuleFinding[];
|
|
25
|
+
export function functionFindings(name: string, fn: string, raw: unknown, file?: string): RuleFinding[];
|
|
26
|
+
export function serviceFindings(name: string, service: unknown, nodeTypes?: Set<string> | null, file?: string): RuleFinding[];
|
|
27
|
+
/** api/make.yaml: the private make function, one function with no name inside it; api/service.yaml never declares it. */
|
|
28
|
+
export function makeFindings(name: string, make: unknown, service: unknown, nodeTypes?: Set<string> | null, file?: string): RuleFinding[];
|
|
29
|
+
export function rowKeyFindings(name: string, envelope: unknown, props: unknown, file?: string): RuleFinding[];
|
|
30
|
+
export function objectTestFindings(name: string, test: unknown, service: unknown, file?: string): RuleFinding[];
|
|
31
|
+
/** The words a `make` block says about its read; every other key is an object that read makes. */
|
|
32
|
+
export const PIPELINE_READ_KEYS: Set<string>;
|
|
33
|
+
export function pipelineFindings(name: string, file: unknown, fileName?: string): RuleFinding[];
|
|
34
|
+
export function collectRefs(value: unknown): Set<string>;
|
|
35
|
+
export function configRefs(value: unknown): Set<string>;
|
|
36
|
+
export function pipelineFolderFindings(
|
|
37
|
+
name: string,
|
|
38
|
+
parts: { file: unknown; config: unknown; test: unknown },
|
|
39
|
+
seen?: { nodeTypes?: Set<string> | null; objectNames?: Set<string> | null },
|
|
40
|
+
): RuleFinding[];
|
|
41
|
+
/** The first error's message; null when none. */
|
|
42
|
+
export function firstError(findings: RuleFinding[]): string | null;
|
|
43
|
+
/** The first finding a loader cannot read past; null when the file loads. */
|
|
44
|
+
export function firstRefusal(findings: RuleFinding[]): string | null;
|