@ekanos/cli 0.17.1 → 0.19.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 (73) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/add.js +1 -1
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/catalog.js +11 -7
  5. package/dist/commands/catalog.js.map +1 -1
  6. package/dist/commands/names.d.ts +19 -0
  7. package/dist/commands/names.js +122 -0
  8. package/dist/commands/names.js.map +1 -0
  9. package/dist/commands/publish.d.ts +53 -3
  10. package/dist/commands/publish.js +58 -16
  11. package/dist/commands/publish.js.map +1 -1
  12. package/dist/commands/sources.js +15 -5
  13. package/dist/commands/sources.js.map +1 -1
  14. package/dist/commands/status.d.ts +2 -0
  15. package/dist/commands/status.js +12 -12
  16. package/dist/commands/status.js.map +1 -1
  17. package/dist/commands/sync.js +2 -2
  18. package/dist/commands/sync.js.map +1 -1
  19. package/dist/commands/teams-connect.js +1 -1
  20. package/dist/commands/teams-connect.js.map +1 -1
  21. package/dist/experience/boundary.d.ts +2 -2
  22. package/dist/experience/boundary.js +2 -11
  23. package/dist/experience/boundary.js.map +1 -1
  24. package/dist/experience/build.d.ts +3 -1
  25. package/dist/experience/build.js +15 -26
  26. package/dist/experience/build.js.map +1 -1
  27. package/dist/experience/clean-room.d.ts +40 -0
  28. package/dist/experience/clean-room.js +150 -0
  29. package/dist/experience/clean-room.js.map +1 -0
  30. package/dist/experience/contract-sources.d.ts +7 -2
  31. package/dist/experience/contract-sources.js +15 -8
  32. package/dist/experience/contract-sources.js.map +1 -1
  33. package/dist/experience/definition-checks.d.ts +12 -1
  34. package/dist/experience/definition-checks.js +34 -11
  35. package/dist/experience/definition-checks.js.map +1 -1
  36. package/dist/experience/definition-shape.d.ts +20 -2
  37. package/dist/experience/definition-shape.js.map +1 -1
  38. package/dist/experience/project.js +8 -2
  39. package/dist/experience/project.js.map +1 -1
  40. package/dist/experience/publish.d.ts +42 -5
  41. package/dist/experience/publish.js +195 -11
  42. package/dist/experience/publish.js.map +1 -1
  43. package/dist/harness-scaffold.js +3 -1
  44. package/dist/harness-scaffold.js.map +1 -1
  45. package/dist/index.js +25 -2
  46. package/dist/index.js.map +1 -1
  47. package/dist/names-api.d.ts +90 -0
  48. package/dist/names-api.js +123 -0
  49. package/dist/names-api.js.map +1 -0
  50. package/dist/product-kinds.d.ts +13 -0
  51. package/dist/product-kinds.js +17 -0
  52. package/dist/product-kinds.js.map +1 -0
  53. package/dist/publish-api.d.ts +38 -5
  54. package/dist/publish-api.js +59 -16
  55. package/dist/publish-api.js.map +1 -1
  56. package/dist/repo/publish.d.ts +6 -3
  57. package/dist/repo/publish.js +48 -14
  58. package/dist/repo/publish.js.map +1 -1
  59. package/dist/source-products-api.d.ts +3 -2
  60. package/dist/source-products-api.js +1 -1
  61. package/dist/source-products-api.js.map +1 -1
  62. package/dist/team-connect-api.d.ts +4 -0
  63. package/dist/team-connect-api.js +1 -1
  64. package/dist/team-connect-api.js.map +1 -1
  65. package/dist/toolchain-api.d.ts +15 -0
  66. package/dist/toolchain-api.js +12 -1
  67. package/dist/toolchain-api.js.map +1 -1
  68. package/package.json +2 -2
  69. package/templates/experience/package.json.tmpl +7 -4
  70. package/templates/harness/app/experience/layout/page.tsx.tmpl +1 -0
  71. package/templates/harness/app/experience/layout.tsx.tmpl +3 -0
  72. package/templates/harness/app/experience/pages/[id]/[...path]/page.tsx.tmpl +1 -0
  73. package/templates/package.json.tmpl +4 -4
@@ -0,0 +1,150 @@
1
+ import { EXPERIENCE_BUNDLE_OPTIONS, buildExperienceSurfaces, experienceExternals, } from '@ekanos/integration-schema';
2
+ import { build } from 'esbuild';
3
+ import { execFileSync } from 'node:child_process';
4
+ import * as fs from 'node:fs';
5
+ import * as os from 'node:os';
6
+ import * as path from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { preconditionError, validationError } from '../errors.js';
9
+ /** Set to `off` to skip the clean-room build (offline work, tests); the host's gate still rebuilds. */
10
+ export const CLEAN_ROOM_ENV = 'EKANOS_PUBLISH_CLEAN_ROOM';
11
+ /** The npm binary the clean room runs. Defaults to `npm`; overridden by tests. */
12
+ export const NPM_BIN_ENV = 'EKANOS_NPM_BIN';
13
+ const LOCKFILE_ARGS = ['--ignore-scripts', '--no-audit', '--no-fund'];
14
+ /** The lockfile the project itself carries, if it carries one. */
15
+ function ownLockfile(dir) {
16
+ for (const [name, source] of [
17
+ ['npm-shrinkwrap.json', 'npm-shrinkwrap'],
18
+ ['package-lock.json', 'package-lock'],
19
+ ]) {
20
+ const file = path.join(dir, name);
21
+ if (fs.existsSync(file))
22
+ return { file, source };
23
+ }
24
+ return null;
25
+ }
26
+ /** A `pnpm-lock.yaml` at or above the project: it lives in a pnpm workspace. */
27
+ export function findPnpmLockfile(dir) {
28
+ let current = path.resolve(dir);
29
+ for (;;) {
30
+ const candidate = path.join(current, 'pnpm-lock.yaml');
31
+ if (fs.existsSync(candidate))
32
+ return candidate;
33
+ const parent = path.dirname(current);
34
+ if (parent === current)
35
+ return null;
36
+ current = parent;
37
+ }
38
+ }
39
+ /** Whether the project has a lockfile of either kind to build from. */
40
+ export function hasBuildableLockfile(dir) {
41
+ return ownLockfile(dir) !== null || findPnpmLockfile(dir) !== null;
42
+ }
43
+ function npm(bin, args, cwd, what) {
44
+ try {
45
+ execFileSync(bin, [...args], {
46
+ cwd,
47
+ encoding: 'utf8',
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ });
50
+ }
51
+ catch (error) {
52
+ const detail = error instanceof Error && 'stderr' in error
53
+ ? String(error.stderr)
54
+ .trim()
55
+ .slice(-1500)
56
+ : String(error);
57
+ throw preconditionError(`${what} failed in the clean room: ${detail}`, 'The host rebuilds your package the same way, from the public registry; fix the dependency problem and re-run.');
58
+ }
59
+ }
60
+ function copyIfExists(from, to) {
61
+ if (fs.existsSync(from))
62
+ fs.cpSync(from, to, { recursive: true });
63
+ }
64
+ /**
65
+ * Builds the experience bundle the way the host's gate will, so the bytes match
66
+ * by construction: in a temp directory holding only what ships (package.json,
67
+ * tsconfig, `src/`), with dependencies installed by `npm ci --ignore-scripts`
68
+ * from a standalone lockfile, and the shared bundle options.
69
+ *
70
+ * The lockfile is the project's own `package-lock.json`/`npm-shrinkwrap.json`
71
+ * when it has one. A project inside a pnpm workspace (a `pnpm-lock.yaml` at the
72
+ * repo root, no per-package npm lockfile) gets one generated for just its
73
+ * `package.json` (`npm install`), and that generated file
74
+ * is what ships, so the host resolves the dependencies the bundle was built
75
+ * with. It resolves the declared ranges from the registry, which can differ from
76
+ * the versions the workspace's pnpm lockfile pins; the bundle you ship is the
77
+ * one built from the shipped lockfile, not from your workspace's tree.
78
+ *
79
+ * The surfaces are recomputed from the rebuilt bundle's own definition (the host
80
+ * does the same), so a project whose local tree declares something else is
81
+ * caught here instead of at the gate.
82
+ */
83
+ export async function buildInCleanRoom(project, env = {}) {
84
+ var _a;
85
+ var _b, _c;
86
+ const npmBin = (_b = env[NPM_BIN_ENV]) !== null && _b !== void 0 ? _b : 'npm';
87
+ const own = ownLockfile(project.dir);
88
+ const pnpm = own ? null : findPnpmLockfile(project.dir);
89
+ if (!own && !pnpm) {
90
+ throw preconditionError('There is no lockfile to build from: no package-lock.json, npm-shrinkwrap.json or pnpm-lock.yaml.', 'Run "npm install --package-lock-only" in the project (or install with pnpm in a workspace) and commit the lockfile.');
91
+ }
92
+ const room = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-exp-room-'));
93
+ try {
94
+ fs.copyFileSync(path.join(project.dir, 'package.json'), path.join(room, 'package.json'));
95
+ copyIfExists(path.join(project.dir, 'src'), path.join(room, 'src'));
96
+ for (const name of fs.readdirSync(project.dir)) {
97
+ if (/^tsconfig[^/]*\.json$/.test(name)) {
98
+ fs.copyFileSync(path.join(project.dir, name), path.join(room, name));
99
+ }
100
+ }
101
+ let lockfileSource;
102
+ if (own) {
103
+ fs.copyFileSync(own.file, path.join(room, 'package-lock.json'));
104
+ lockfileSource = own.source;
105
+ }
106
+ else {
107
+ const manifest = fs.readFileSync(path.join(room, 'package.json'), 'utf8');
108
+ if (/"(?:workspace|catalog|link|file):/.test(manifest)) {
109
+ throw validationError('package.json depends on a workspace:, catalog:, link: or file: package, which a standalone lockfile cannot resolve.', 'An experience reaches integrations through pinned operations, not packages; publish a package that depends only on registry versions.');
110
+ }
111
+ // A real install, twice, not `--package-lock-only`: npm resolving an
112
+ // empty tree writes lockfiles that `npm ci` then rejects as out of sync
113
+ // (a peer range resolved two ways), and a first install over a fresh tree
114
+ // can too (seen with eslint's ajv). The second install reconciles the
115
+ // tree against its own lockfile; the `npm ci` below, after the tree is
116
+ // thrown away, is the install the host will repeat and proves it did.
117
+ for (let pass = 0; pass < 2; pass += 1) {
118
+ npm(npmBin, ['install', ...LOCKFILE_ARGS], room, 'generating a standalone lockfile from package.json');
119
+ }
120
+ fs.rmSync(path.join(room, 'node_modules'), {
121
+ recursive: true,
122
+ force: true,
123
+ });
124
+ lockfileSource = 'generated-from-pnpm';
125
+ }
126
+ const lockfile = fs.readFileSync(path.join(room, 'package-lock.json'), 'utf8');
127
+ npm(npmBin, ['ci', ...LOCKFILE_ARGS], room, '`npm ci`');
128
+ const built = await build(Object.assign(Object.assign({}, EXPERIENCE_BUNDLE_OPTIONS), { entryPoints: [path.join(room, project.entry)], absWorkingDir: room, external: experienceExternals(project.ekanos.allowImports) }));
129
+ const bundle = (_c = (_a = built.outputFiles[0]) === null || _a === void 0 ? void 0 : _a.text) !== null && _c !== void 0 ? _c : '';
130
+ const file = path.join(room, '.clean-room-experience.mjs');
131
+ fs.writeFileSync(file, bundle);
132
+ const loaded = (await import(pathToFileURL(file).href));
133
+ if (typeof loaded.default !== 'object' ||
134
+ loaded.default === null ||
135
+ !Array.isArray(loaded.default.widgets) ||
136
+ !Array.isArray(loaded.default.pages)) {
137
+ throw validationError('The rebuilt bundle does not export a defineExperience() result as default.', `Export the definition from ${project.entry} as the default export.`);
138
+ }
139
+ return {
140
+ bundle,
141
+ lockfile,
142
+ lockfileSource,
143
+ surfaces: buildExperienceSurfaces(project.slug, loaded.default),
144
+ };
145
+ }
146
+ finally {
147
+ fs.rmSync(room, { recursive: true, force: true });
148
+ }
149
+ }
150
+ //# sourceMappingURL=clean-room.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clean-room.js","sourceRoot":"","sources":["../../src/experience/clean-room.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n EXPERIENCE_BUNDLE_OPTIONS,\n type ExperienceSurfaces,\n type SurfacesDefinitionInput,\n buildExperienceSurfaces,\n experienceExternals,\n} from '@ekanos/integration-schema';\nimport { build } from 'esbuild';\nimport { execFileSync } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { preconditionError, validationError } from '../errors';\nimport type { ExperienceProject } from './project';\n\n/** Set to `off` to skip the clean-room build (offline work, tests); the host's gate still rebuilds. */\nexport const CLEAN_ROOM_ENV = 'EKANOS_PUBLISH_CLEAN_ROOM';\n/** The npm binary the clean room runs. Defaults to `npm`; overridden by tests. */\nexport const NPM_BIN_ENV = 'EKANOS_NPM_BIN';\n\nexport type LockfileSource =\n | 'package-lock'\n | 'npm-shrinkwrap'\n | 'generated-from-pnpm';\n\nexport interface CleanRoomBuild {\n /** `dist/experience.js` exactly as the host's gate will rebuild it. */\n bundle: string;\n /** The standalone lockfile the bundle was built against; ships as npm-shrinkwrap.json. */\n lockfile: string;\n lockfileSource: LockfileSource;\n /** `dist/surfaces.json` as the rebuilt bundle's own definition declares it. */\n surfaces: ExperienceSurfaces;\n}\n\nconst LOCKFILE_ARGS = ['--ignore-scripts', '--no-audit', '--no-fund'] as const;\n\n/** The lockfile the project itself carries, if it carries one. */\nfunction ownLockfile(\n dir: string,\n): { file: string; source: 'package-lock' | 'npm-shrinkwrap' } | null {\n for (const [name, source] of [\n ['npm-shrinkwrap.json', 'npm-shrinkwrap'],\n ['package-lock.json', 'package-lock'],\n ] as const) {\n const file = path.join(dir, name);\n if (fs.existsSync(file)) return { file, source };\n }\n\n return null;\n}\n\n/** A `pnpm-lock.yaml` at or above the project: it lives in a pnpm workspace. */\nexport function findPnpmLockfile(dir: string): string | null {\n let current = path.resolve(dir);\n for (;;) {\n const candidate = path.join(current, 'pnpm-lock.yaml');\n if (fs.existsSync(candidate)) return candidate;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n}\n\n/** Whether the project has a lockfile of either kind to build from. */\nexport function hasBuildableLockfile(dir: string): boolean {\n return ownLockfile(dir) !== null || findPnpmLockfile(dir) !== null;\n}\n\nfunction npm(\n bin: string,\n args: readonly string[],\n cwd: string,\n what: string,\n): void {\n try {\n execFileSync(bin, [...args], {\n cwd,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n } catch (error) {\n const detail =\n error instanceof Error && 'stderr' in error\n ? String((error as { stderr: unknown }).stderr)\n .trim()\n .slice(-1500)\n : String(error);\n throw preconditionError(\n `${what} failed in the clean room: ${detail}`,\n 'The host rebuilds your package the same way, from the public registry; fix the dependency problem and re-run.',\n );\n }\n}\n\nfunction copyIfExists(from: string, to: string): void {\n if (fs.existsSync(from)) fs.cpSync(from, to, { recursive: true });\n}\n\n/**\n * Builds the experience bundle the way the host's gate will, so the bytes match\n * by construction: in a temp directory holding only what ships (package.json,\n * tsconfig, `src/`), with dependencies installed by `npm ci --ignore-scripts`\n * from a standalone lockfile, and the shared bundle options.\n *\n * The lockfile is the project's own `package-lock.json`/`npm-shrinkwrap.json`\n * when it has one. A project inside a pnpm workspace (a `pnpm-lock.yaml` at the\n * repo root, no per-package npm lockfile) gets one generated for just its\n * `package.json` (`npm install`), and that generated file\n * is what ships, so the host resolves the dependencies the bundle was built\n * with. It resolves the declared ranges from the registry, which can differ from\n * the versions the workspace's pnpm lockfile pins; the bundle you ship is the\n * one built from the shipped lockfile, not from your workspace's tree.\n *\n * The surfaces are recomputed from the rebuilt bundle's own definition (the host\n * does the same), so a project whose local tree declares something else is\n * caught here instead of at the gate.\n */\nexport async function buildInCleanRoom(\n project: ExperienceProject,\n env: Record<string, string | undefined> = {},\n): Promise<CleanRoomBuild> {\n const npmBin = env[NPM_BIN_ENV] ?? 'npm';\n const own = ownLockfile(project.dir);\n const pnpm = own ? null : findPnpmLockfile(project.dir);\n\n if (!own && !pnpm) {\n throw preconditionError(\n 'There is no lockfile to build from: no package-lock.json, npm-shrinkwrap.json or pnpm-lock.yaml.',\n 'Run \"npm install --package-lock-only\" in the project (or install with pnpm in a workspace) and commit the lockfile.',\n );\n }\n\n const room = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-exp-room-'));\n try {\n fs.copyFileSync(\n path.join(project.dir, 'package.json'),\n path.join(room, 'package.json'),\n );\n copyIfExists(path.join(project.dir, 'src'), path.join(room, 'src'));\n for (const name of fs.readdirSync(project.dir)) {\n if (/^tsconfig[^/]*\\.json$/.test(name)) {\n fs.copyFileSync(path.join(project.dir, name), path.join(room, name));\n }\n }\n\n let lockfileSource: LockfileSource;\n if (own) {\n fs.copyFileSync(own.file, path.join(room, 'package-lock.json'));\n lockfileSource = own.source;\n } else {\n const manifest = fs.readFileSync(path.join(room, 'package.json'), 'utf8');\n if (/\"(?:workspace|catalog|link|file):/.test(manifest)) {\n throw validationError(\n 'package.json depends on a workspace:, catalog:, link: or file: package, which a standalone lockfile cannot resolve.',\n 'An experience reaches integrations through pinned operations, not packages; publish a package that depends only on registry versions.',\n );\n }\n // A real install, twice, not `--package-lock-only`: npm resolving an\n // empty tree writes lockfiles that `npm ci` then rejects as out of sync\n // (a peer range resolved two ways), and a first install over a fresh tree\n // can too (seen with eslint's ajv). The second install reconciles the\n // tree against its own lockfile; the `npm ci` below, after the tree is\n // thrown away, is the install the host will repeat and proves it did.\n for (let pass = 0; pass < 2; pass += 1) {\n npm(\n npmBin,\n ['install', ...LOCKFILE_ARGS],\n room,\n 'generating a standalone lockfile from package.json',\n );\n }\n fs.rmSync(path.join(room, 'node_modules'), {\n recursive: true,\n force: true,\n });\n lockfileSource = 'generated-from-pnpm';\n }\n\n const lockfile = fs.readFileSync(\n path.join(room, 'package-lock.json'),\n 'utf8',\n );\n npm(npmBin, ['ci', ...LOCKFILE_ARGS], room, '`npm ci`');\n\n const built = await build({\n ...EXPERIENCE_BUNDLE_OPTIONS,\n entryPoints: [path.join(room, project.entry)],\n absWorkingDir: room,\n external: experienceExternals(project.ekanos.allowImports),\n });\n const bundle = built.outputFiles[0]?.text ?? '';\n\n const file = path.join(room, '.clean-room-experience.mjs');\n fs.writeFileSync(file, bundle);\n const loaded = (await import(pathToFileURL(file).href)) as {\n default?: SurfacesDefinitionInput;\n };\n if (\n typeof loaded.default !== 'object' ||\n loaded.default === null ||\n !Array.isArray(loaded.default.widgets) ||\n !Array.isArray(loaded.default.pages)\n ) {\n throw validationError(\n 'The rebuilt bundle does not export a defineExperience() result as default.',\n `Export the definition from ${project.entry} as the default export.`,\n );\n }\n\n return {\n bundle,\n lockfile,\n lockfileSource,\n surfaces: buildExperienceSurfaces(project.slug, loaded.default),\n };\n } finally {\n fs.rmSync(room, { recursive: true, force: true });\n }\n}\n"]}
@@ -22,11 +22,16 @@ export interface SourceOptions {
22
22
  fromPath?: string;
23
23
  env?: Record<string, string | undefined>;
24
24
  }
25
- /** Every `*.contract.json` of `dir` for `integration`, highest contract version within `line` first. */
25
+ /**
26
+ * Every `*.contract.json` of `dir` for `integration` (a reference: `slug` or
27
+ * `ns/slug`; files are named by its token), highest contract version within
28
+ * `line` first. A namespaced reference never matches a global file of the same
29
+ * slug, nor the reverse.
30
+ */
26
31
  export declare function contractsInDir(dir: string, integration: string, line: string): Contract[];
27
32
  export declare function dirSource(dir: string): ContractSource;
28
33
  export declare function pathSource(projectPath: string): ContractSource;
29
- /** The committed pin: `<contractsDir>/<integration>@<version>.contract.json`. */
34
+ /** The committed pin: `<contractsDir>/<token>@<version>.contract.json`, always flat. */
30
35
  export declare function snapshotFile(project: ExperienceProject, integration: string, line: string): string;
31
36
  export declare function snapshotSource(project: ExperienceProject): ContractSource;
32
37
  /** Sources in resolution order: explicit flags, env, then (optionally) the snapshot. */
@@ -1,4 +1,4 @@
1
- import { compareSemver, compatLine, verifyContract, } from '@ekanos/integration-schema';
1
+ import { compareSemver, compatLine, contractMatchesRef, integrationRefToken, snapshotFileName, verifyContract, } from '@ekanos/integration-schema';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { notFoundError, validationError } from '../errors.js';
@@ -17,8 +17,16 @@ function readContractFile(file) {
17
17
  }
18
18
  return verified.contract;
19
19
  }
20
- /** Every `*.contract.json` of `dir` for `integration`, highest contract version within `line` first. */
20
+ /**
21
+ * Every `*.contract.json` of `dir` for `integration` (a reference: `slug` or
22
+ * `ns/slug`; files are named by its token), highest contract version within
23
+ * `line` first. A namespaced reference never matches a global file of the same
24
+ * slug, nor the reverse.
25
+ */
21
26
  export function contractsInDir(dir, integration, line) {
27
+ const token = integrationRefToken(integration);
28
+ if (token === null)
29
+ return [];
22
30
  let names;
23
31
  try {
24
32
  names = fs.readdirSync(dir).filter((n) => n.endsWith('.contract.json'));
@@ -28,13 +36,12 @@ export function contractsInDir(dir, integration, line) {
28
36
  }
29
37
  const found = [];
30
38
  for (const name of names) {
31
- if (!name.startsWith(`${integration}@`) &&
32
- !name.startsWith(`${integration}.`)) {
39
+ if (!name.startsWith(`${token}@`) && !name.startsWith(`${token}.`)) {
33
40
  continue;
34
41
  }
35
42
  const contract = readContractFile(path.join(dir, name));
36
43
  if (contract &&
37
- contract.integration === integration &&
44
+ contractMatchesRef(contract, integration) &&
38
45
  contract.compatLine === line) {
39
46
  found.push(contract);
40
47
  }
@@ -60,7 +67,7 @@ export function pathSource(projectPath) {
60
67
  }
61
68
  const contract = readContractFile(file);
62
69
  if (!contract ||
63
- contract.integration !== integration ||
70
+ !contractMatchesRef(contract, integration) ||
64
71
  contract.compatLine !== line) {
65
72
  return null;
66
73
  }
@@ -68,9 +75,9 @@ export function pathSource(projectPath) {
68
75
  },
69
76
  };
70
77
  }
71
- /** The committed pin: `<contractsDir>/<integration>@<version>.contract.json`. */
78
+ /** The committed pin: `<contractsDir>/<token>@<version>.contract.json`, always flat. */
72
79
  export function snapshotFile(project, integration, line) {
73
- return path.join(project.contractsDir, `${integration}@${line}.contract.json`);
80
+ return path.join(project.contractsDir, snapshotFileName(integration, line));
74
81
  }
75
82
  export function snapshotSource(project) {
76
83
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"contract-sources.js","sourceRoot":"","sources":["../../src/experience/contract-sources.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Contract,\n compareSemver,\n compatLine,\n verifyContract,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { notFoundError, validationError } from '../errors';\nimport type { ExperienceProject } from './project';\n\n/** Where an integration's contract came from, for messages and `sync` bookkeeping. */\nexport interface ResolvedContract {\n contract: Contract;\n origin: string;\n kind: 'dir' | 'path' | 'snapshot';\n}\n\n/**\n * A pluggable contract source. The dir/path/snapshot sources below are the\n * offline defaults; the harness resolver (catalog, published packages) plugs\n * in here as another source, so `add`/`sync` never learn where contracts live.\n */\nexport interface ContractSource {\n name: string;\n resolve(\n integration: string,\n line: string,\n ): Promise<ResolvedContract | null> | ResolvedContract | null;\n}\n\nexport interface SourceOptions {\n /** A directory of `*.contract.json` (the monorepo contracts folder or a registry mirror). */\n fromDir?: string;\n /** An integration project directory; its `contract.json` is read. */\n fromPath?: string;\n env?: Record<string, string | undefined>;\n}\n\nfunction readContractFile(file: string): Contract | null {\n let json: unknown;\n try {\n json = JSON.parse(fs.readFileSync(file, 'utf8'));\n } catch {\n return null;\n }\n const verified = verifyContract(json);\n if (!verified.contract || verified.problems.length > 0) {\n const first = verified.problems[0];\n throw validationError(\n `${file} is not a valid contract: ${first ? `${first.rule}: ${first.message}` : 'unreadable'}`,\n 'Regenerate it with \"ekanos contract\" in the integration, or point at a valid contracts directory.',\n );\n }\n\n return verified.contract;\n}\n\n/** Every `*.contract.json` of `dir` for `integration`, highest contract version within `line` first. */\nexport function contractsInDir(\n dir: string,\n integration: string,\n line: string,\n): Contract[] {\n let names: string[];\n try {\n names = fs.readdirSync(dir).filter((n) => n.endsWith('.contract.json'));\n } catch {\n return [];\n }\n const found: Contract[] = [];\n for (const name of names) {\n if (\n !name.startsWith(`${integration}@`) &&\n !name.startsWith(`${integration}.`)\n ) {\n continue;\n }\n const contract = readContractFile(path.join(dir, name));\n if (\n contract &&\n contract.integration === integration &&\n contract.compatLine === line\n ) {\n found.push(contract);\n }\n }\n\n return found.sort((a, b) =>\n compareSemver(b.contractVersion, a.contractVersion),\n );\n}\n\nexport function dirSource(dir: string): ContractSource {\n return {\n name: `dir ${dir}`,\n resolve(integration, line) {\n const [best] = contractsInDir(dir, integration, line);\n\n return best ? { contract: best, origin: dir, kind: 'dir' } : null;\n },\n };\n}\n\nexport function pathSource(projectPath: string): ContractSource {\n return {\n name: `path ${projectPath}`,\n resolve(integration, line) {\n const file = path.join(path.resolve(projectPath), 'contract.json');\n if (!fs.existsSync(file)) {\n throw notFoundError(\n `${file} does not exist.`,\n 'Run \"ekanos contract\" in that integration to derive its contract.json.',\n );\n }\n const contract = readContractFile(file);\n if (\n !contract ||\n contract.integration !== integration ||\n contract.compatLine !== line\n ) {\n return null;\n }\n\n return { contract, origin: file, kind: 'path' };\n },\n };\n}\n\n/** The committed pin: `<contractsDir>/<integration>@<version>.contract.json`. */\nexport function snapshotFile(\n project: ExperienceProject,\n integration: string,\n line: string,\n): string {\n return path.join(\n project.contractsDir,\n `${integration}@${line}.contract.json`,\n );\n}\n\nexport function snapshotSource(project: ExperienceProject): ContractSource {\n return {\n name: 'committed snapshot',\n resolve(integration, line) {\n const file = snapshotFile(project, integration, line);\n const contract = fs.existsSync(file) ? readContractFile(file) : null;\n\n return contract ? { contract, origin: file, kind: 'snapshot' } : null;\n },\n };\n}\n\n/** Sources in resolution order: explicit flags, env, then (optionally) the snapshot. */\nexport function defaultSources(\n project: ExperienceProject,\n options: SourceOptions,\n extra: readonly ContractSource[] = [],\n includeSnapshot = true,\n): ContractSource[] {\n const sources: ContractSource[] = [];\n if (options.fromPath) sources.push(pathSource(options.fromPath));\n if (options.fromDir) sources.push(dirSource(path.resolve(options.fromDir)));\n const envDir = options.env?.EKANOS_CONTRACTS_DIR;\n if (!options.fromDir && envDir) sources.push(dirSource(path.resolve(envDir)));\n sources.push(...extra);\n if (includeSnapshot) sources.push(snapshotSource(project));\n\n return sources;\n}\n\nexport async function resolveContract(\n sources: readonly ContractSource[],\n integration: string,\n line: string,\n): Promise<ResolvedContract> {\n for (const source of sources) {\n const hit = await source.resolve(integration, line);\n if (hit) return hit;\n }\n\n throw notFoundError(\n `No contract for ${integration}@${line} (looked in: ${sources.map((s) => s.name).join(', ') || 'nothing'}).`,\n 'Pass --from-dir <contracts folder> or --from-path <integration project>, or set EKANOS_CONTRACTS_DIR.',\n );\n}\n\n/** Line for a package version, re-exported so callers need no schema import. */\nexport { compatLine };\n"]}
1
+ {"version":3,"file":"contract-sources.js","sourceRoot":"","sources":["../../src/experience/contract-sources.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Contract,\n compareSemver,\n compatLine,\n contractMatchesRef,\n integrationRefToken,\n snapshotFileName,\n verifyContract,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { notFoundError, validationError } from '../errors';\nimport type { ExperienceProject } from './project';\n\n/** Where an integration's contract came from, for messages and `sync` bookkeeping. */\nexport interface ResolvedContract {\n contract: Contract;\n origin: string;\n kind: 'dir' | 'path' | 'snapshot';\n}\n\n/**\n * A pluggable contract source. The dir/path/snapshot sources below are the\n * offline defaults; the harness resolver (catalog, published packages) plugs\n * in here as another source, so `add`/`sync` never learn where contracts live.\n */\nexport interface ContractSource {\n name: string;\n resolve(\n integration: string,\n line: string,\n ): Promise<ResolvedContract | null> | ResolvedContract | null;\n}\n\nexport interface SourceOptions {\n /** A directory of `*.contract.json` (the monorepo contracts folder or a registry mirror). */\n fromDir?: string;\n /** An integration project directory; its `contract.json` is read. */\n fromPath?: string;\n env?: Record<string, string | undefined>;\n}\n\nfunction readContractFile(file: string): Contract | null {\n let json: unknown;\n try {\n json = JSON.parse(fs.readFileSync(file, 'utf8'));\n } catch {\n return null;\n }\n const verified = verifyContract(json);\n if (!verified.contract || verified.problems.length > 0) {\n const first = verified.problems[0];\n throw validationError(\n `${file} is not a valid contract: ${first ? `${first.rule}: ${first.message}` : 'unreadable'}`,\n 'Regenerate it with \"ekanos contract\" in the integration, or point at a valid contracts directory.',\n );\n }\n\n return verified.contract;\n}\n\n/**\n * Every `*.contract.json` of `dir` for `integration` (a reference: `slug` or\n * `ns/slug`; files are named by its token), highest contract version within\n * `line` first. A namespaced reference never matches a global file of the same\n * slug, nor the reverse.\n */\nexport function contractsInDir(\n dir: string,\n integration: string,\n line: string,\n): Contract[] {\n const token = integrationRefToken(integration);\n if (token === null) return [];\n let names: string[];\n try {\n names = fs.readdirSync(dir).filter((n) => n.endsWith('.contract.json'));\n } catch {\n return [];\n }\n const found: Contract[] = [];\n for (const name of names) {\n if (!name.startsWith(`${token}@`) && !name.startsWith(`${token}.`)) {\n continue;\n }\n const contract = readContractFile(path.join(dir, name));\n if (\n contract &&\n contractMatchesRef(contract, integration) &&\n contract.compatLine === line\n ) {\n found.push(contract);\n }\n }\n\n return found.sort((a, b) =>\n compareSemver(b.contractVersion, a.contractVersion),\n );\n}\n\nexport function dirSource(dir: string): ContractSource {\n return {\n name: `dir ${dir}`,\n resolve(integration, line) {\n const [best] = contractsInDir(dir, integration, line);\n\n return best ? { contract: best, origin: dir, kind: 'dir' } : null;\n },\n };\n}\n\nexport function pathSource(projectPath: string): ContractSource {\n return {\n name: `path ${projectPath}`,\n resolve(integration, line) {\n const file = path.join(path.resolve(projectPath), 'contract.json');\n if (!fs.existsSync(file)) {\n throw notFoundError(\n `${file} does not exist.`,\n 'Run \"ekanos contract\" in that integration to derive its contract.json.',\n );\n }\n const contract = readContractFile(file);\n if (\n !contract ||\n !contractMatchesRef(contract, integration) ||\n contract.compatLine !== line\n ) {\n return null;\n }\n\n return { contract, origin: file, kind: 'path' };\n },\n };\n}\n\n/** The committed pin: `<contractsDir>/<token>@<version>.contract.json`, always flat. */\nexport function snapshotFile(\n project: ExperienceProject,\n integration: string,\n line: string,\n): string {\n return path.join(project.contractsDir, snapshotFileName(integration, line));\n}\n\nexport function snapshotSource(project: ExperienceProject): ContractSource {\n return {\n name: 'committed snapshot',\n resolve(integration, line) {\n const file = snapshotFile(project, integration, line);\n const contract = fs.existsSync(file) ? readContractFile(file) : null;\n\n return contract ? { contract, origin: file, kind: 'snapshot' } : null;\n },\n };\n}\n\n/** Sources in resolution order: explicit flags, env, then (optionally) the snapshot. */\nexport function defaultSources(\n project: ExperienceProject,\n options: SourceOptions,\n extra: readonly ContractSource[] = [],\n includeSnapshot = true,\n): ContractSource[] {\n const sources: ContractSource[] = [];\n if (options.fromPath) sources.push(pathSource(options.fromPath));\n if (options.fromDir) sources.push(dirSource(path.resolve(options.fromDir)));\n const envDir = options.env?.EKANOS_CONTRACTS_DIR;\n if (!options.fromDir && envDir) sources.push(dirSource(path.resolve(envDir)));\n sources.push(...extra);\n if (includeSnapshot) sources.push(snapshotSource(project));\n\n return sources;\n}\n\nexport async function resolveContract(\n sources: readonly ContractSource[],\n integration: string,\n line: string,\n): Promise<ResolvedContract> {\n for (const source of sources) {\n const hit = await source.resolve(integration, line);\n if (hit) return hit;\n }\n\n throw notFoundError(\n `No contract for ${integration}@${line} (looked in: ${sources.map((s) => s.name).join(', ') || 'nothing'}).`,\n 'Pass --from-dir <contracts folder> or --from-path <integration project>, or set EKANOS_CONTRACTS_DIR.',\n );\n}\n\n/** Line for a package version, re-exported so callers need no schema import. */\nexport { compatLine };\n"]}
@@ -1,8 +1,9 @@
1
- import { type ExperienceManifest, type Finding, type WorkflowTemplate } from '@ekanos/integration-schema';
1
+ import { type ExperienceManifest, type ExperienceSurfaces, type Finding, type WorkflowTemplate } from '@ekanos/integration-schema';
2
2
  import type { LoadedExperienceDefinition, LoadedExperienceModule } from './definition-shape.js';
3
3
  import { type PinnedIntegration } from './pins.js';
4
4
  import type { ExperienceProject } from './project.js';
5
5
  export declare const DEFINITION_CHECK = "checks/experience-definition";
6
+ export declare const SURFACES_CHECK: "checks/experience-surfaces";
6
7
  export interface DefinitionAnalysis {
7
8
  findings: Finding[];
8
9
  workflows: WorkflowTemplate[];
@@ -14,6 +15,15 @@ export interface DefinitionAnalysis {
14
15
  * review queue reads only, and each template serializes to the engine's DSL.
15
16
  */
16
17
  export declare function analyzeDefinition(project: ExperienceProject, loaded: LoadedExperienceModule, pinned: readonly PinnedIntegration[]): DefinitionAnalysis;
18
+ /**
19
+ * The surfaces contract: page kinds, canvas widgets, routes and the suggested
20
+ * layout must be well formed and refer only to what the experience declares.
21
+ * The same rules `defineExperience` enforces, re-run on the loaded definition
22
+ * so a definition built by an older SDK, or by hand, cannot slip past.
23
+ */
24
+ export declare function checkSurfaces(project: ExperienceProject, definition: LoadedExperienceDefinition): Finding[];
25
+ /** `dist/surfaces.json`: the surfaces and suggested layout as data, no code. */
26
+ export declare function buildSurfaces(project: ExperienceProject, definition: LoadedExperienceDefinition): ExperienceSurfaces;
17
27
  /** `fixtures/<integration>/<kind>-<name>.<variant>.json` must satisfy the pinned output schema. */
18
28
  export declare function checkFixtures(project: ExperienceProject, pinned: readonly PinnedIntegration[]): Finding[];
19
29
  export declare function buildManifest(input: {
@@ -23,4 +33,5 @@ export declare function buildManifest(input: {
23
33
  workflows: readonly WorkflowTemplate[];
24
34
  version: string;
25
35
  sdk: string;
36
+ surfaces: ExperienceSurfaces;
26
37
  }): ExperienceManifest;
@@ -1,9 +1,10 @@
1
- import { WorkflowTemplateSchema, canonicalJson, sha256Hex, validateAgainstSchema, workflowTemplateProblems, } from '@ekanos/integration-schema';
1
+ import { ExperienceSurfacesSchema, WorkflowTemplateSchema, EKANOS_CHECK_STAGE_IDS, buildExperienceSurfaces, canonicalJson, sha256Hex, validateAgainstSchema, workflowTemplateProblems, } from '@ekanos/integration-schema';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { declaredOperationKeys } from './integrations-codegen.js';
5
5
  import { effectLookup } from './pins.js';
6
6
  export const DEFINITION_CHECK = 'checks/experience-definition';
7
+ export const SURFACES_CHECK = EKANOS_CHECK_STAGE_IDS.experienceSurfaces;
7
8
  const err = (message, hint, file) => (Object.assign({ check: DEFINITION_CHECK, severity: 'error', message,
8
9
  hint }, (file ? { file } : {})));
9
10
  const warn = (message, hint, file) => (Object.assign({ check: DEFINITION_CHECK, severity: 'warn', message,
@@ -16,7 +17,7 @@ const warn = (message, hint, file) => (Object.assign({ check: DEFINITION_CHECK,
16
17
  */
17
18
  export function analyzeDefinition(project, loaded, pinned) {
18
19
  var _a, _b, _c, _d, _e;
19
- var _f, _g;
20
+ var _f, _g, _h;
20
21
  const { definition } = loaded;
21
22
  const findings = [];
22
23
  const workflows = [];
@@ -44,10 +45,11 @@ export function analyzeDefinition(project, loaded, pinned) {
44
45
  checkIntegration(integration, `widget "${widget.id}"`);
45
46
  }
46
47
  for (const page of definition.pages) {
47
- for (const integration of page.integrations)
48
+ for (const integration of (_f = page.integrations) !== null && _f !== void 0 ? _f : [])
48
49
  checkIntegration(integration, `page "${page.id}"`);
49
50
  }
50
- for (const section of (_f = (_a = definition.reviewQueue) === null || _a === void 0 ? void 0 : _a.sections) !== null && _f !== void 0 ? _f : []) {
51
+ findings.push(...checkSurfaces(project, definition));
52
+ for (const section of (_g = (_a = definition.reviewQueue) === null || _a === void 0 ? void 0 : _a.sections) !== null && _g !== void 0 ? _g : []) {
51
53
  if (!checkIntegration(section.integration, `review section "${section.id}"`))
52
54
  continue;
53
55
  if (!((_b = declared.get(section.integration)) === null || _b === void 0 ? void 0 : _b.has(section.read))) {
@@ -101,7 +103,7 @@ export function analyzeDefinition(project, loaded, pinned) {
101
103
  const gated = new Set();
102
104
  const preds = new Map();
103
105
  for (const e of parsed.data.edges)
104
- preds.set(e.to, [...((_g = preds.get(e.to)) !== null && _g !== void 0 ? _g : []), e.from]);
106
+ preds.set(e.to, [...((_h = preds.get(e.to)) !== null && _h !== void 0 ? _h : []), e.from]);
105
107
  const hasGate = (id, seen = new Set()) => {
106
108
  var _a;
107
109
  if (seen.has(id))
@@ -122,6 +124,30 @@ export function analyzeDefinition(project, loaded, pinned) {
122
124
  }
123
125
  return { findings, workflows };
124
126
  }
127
+ // ─── surfaces ───────────────────────────────────────────────────────────────
128
+ /**
129
+ * The surfaces contract: page kinds, canvas widgets, routes and the suggested
130
+ * layout must be well formed and refer only to what the experience declares.
131
+ * The same rules `defineExperience` enforces, re-run on the loaded definition
132
+ * so a definition built by an older SDK, or by hand, cannot slip past.
133
+ */
134
+ export function checkSurfaces(project, definition) {
135
+ const built = buildSurfaces(project, definition);
136
+ const parsed = ExperienceSurfacesSchema.safeParse(built);
137
+ if (parsed.success)
138
+ return [];
139
+ return parsed.error.issues.map((issue) => ({
140
+ check: SURFACES_CHECK,
141
+ severity: 'error',
142
+ message: `${issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''}${issue.message}`,
143
+ hint: 'Fix the page kinds, canvas widgets or suggestedLayout in the experience definition.',
144
+ file: project.entry,
145
+ }));
146
+ }
147
+ /** `dist/surfaces.json`: the surfaces and suggested layout as data, no code. */
148
+ export function buildSurfaces(project, definition) {
149
+ return buildExperienceSurfaces(project.slug, definition);
150
+ }
125
151
  // ─── fixtures ───────────────────────────────────────────────────────────────
126
152
  const FIXTURE_NAME = /^(tool|action)-(.+)\.(default|empty|error)\.json$/;
127
153
  /** `fixtures/<integration>/<kind>-<name>.<variant>.json` must satisfy the pinned output schema. */
@@ -179,12 +205,9 @@ export function buildManifest(input) {
179
205
  ? {}
180
206
  : { isCollapsible: w.isCollapsible })), (w.aiFooterEnabled === undefined
181
207
  ? {}
182
- : { aiFooterEnabled: w.aiFooterEnabled })))), pages: definition.pages.map((p) => ({
183
- id: p.id,
184
- path: p.path,
185
- title: p.title,
186
- integrations: [...p.integrations],
187
- })), workflowTemplates: [...input.workflows] }), (definition.reviewQueue
208
+ : { aiFooterEnabled: w.aiFooterEnabled })))), pages: definition.pages.map((p) => { var _a; return (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ id: p.id }, (p.path === undefined ? {} : { path: p.path })), { title: p.title, integrations: [...((_a = p.integrations) !== null && _a !== void 0 ? _a : [])] }), (p.kind === undefined ? {} : { kind: p.kind })), (p.widgets === undefined ? {} : { widgets: [...p.widgets] })), (p.routes && p.routes.length > 0
209
+ ? { routes: p.routes.map((r) => ({ path: r.path, title: r.title })) }
210
+ : {}))); }), surfacesDigest: input.surfaces.digest, workflowTemplates: [...input.workflows] }), (definition.reviewQueue
188
211
  ? {
189
212
  reviewQueue: {
190
213
  readsOnly: true,
@@ -1 +1 @@
1
- {"version":3,"file":"definition-checks.js","sourceRoot":"","sources":["../../src/experience/definition-checks.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type ExperienceManifest,\n type Finding,\n type WorkflowTemplate,\n WorkflowTemplateSchema,\n canonicalJson,\n sha256Hex,\n validateAgainstSchema,\n workflowTemplateProblems,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type {\n LoadedExperienceDefinition,\n LoadedExperienceModule,\n} from './definition-shape';\nimport { declaredOperationKeys } from './integrations-codegen';\nimport { type PinnedIntegration, effectLookup } from './pins';\nimport type { ExperienceProject } from './project';\n\nexport const DEFINITION_CHECK = 'checks/experience-definition';\n\nconst err = (message: string, hint: string, file?: string): Finding => ({\n check: DEFINITION_CHECK,\n severity: 'error',\n message,\n hint,\n ...(file ? { file } : {}),\n});\n\nconst warn = (message: string, hint: string, file?: string): Finding => ({\n check: DEFINITION_CHECK,\n severity: 'warn',\n message,\n hint,\n ...(file ? { file } : {}),\n});\n\nexport interface DefinitionAnalysis {\n findings: Finding[];\n workflows: WorkflowTemplate[];\n}\n\n/**\n * Cross-check the loaded definition against `ekanos.json` and the pinned\n * contracts: every integration a widget, page, review section or workflow step names\n * must be declared, every operation must be in that integration's allowlist, the\n * review queue reads only, and each template serializes to the engine's DSL.\n */\nexport function analyzeDefinition(\n project: ExperienceProject,\n loaded: LoadedExperienceModule,\n pinned: readonly PinnedIntegration[],\n): DefinitionAnalysis {\n const { definition } = loaded;\n const findings: Finding[] = [];\n const workflows: WorkflowTemplate[] = [];\n const declaredIntegrations = new Map(\n project.integrations.map((u) => [u.name, u]),\n );\n const declared = new Map(\n project.integrations.map((u) => [\n u.name,\n new Set(declaredOperationKeys(u)),\n ]),\n );\n const contractOps = new Map(\n pinned.map(({ use, contract }) => [\n use.name,\n new Map(contract.operations.map((op) => [`${op.kind}/${op.name}`, op])),\n ]),\n );\n\n if (definition.slug !== project.slug) {\n findings.push(\n err(\n `the definition's slug \"${definition.slug}\" differs from ekanos.json \"${project.slug}\".`,\n 'Make them equal.',\n project.entry,\n ),\n );\n }\n\n const checkIntegration = (integration: string, owner: string) => {\n if (!declaredIntegrations.has(integration)) {\n findings.push(\n err(\n `${owner} uses integration \"${integration}\", which ekanos.json \"integrations\" does not declare.`,\n `Add it with \"ekanos add ${integration}@<version>\".`,\n project.entry,\n ),\n );\n\n return false;\n }\n\n return true;\n };\n\n for (const widget of definition.widgets) {\n for (const integration of widget.integrations)\n checkIntegration(integration, `widget \"${widget.id}\"`);\n }\n for (const page of definition.pages) {\n for (const integration of page.integrations)\n checkIntegration(integration, `page \"${page.id}\"`);\n }\n\n for (const section of definition.reviewQueue?.sections ?? []) {\n if (\n !checkIntegration(section.integration, `review section \"${section.id}\"`)\n )\n continue;\n if (!declared.get(section.integration)?.has(section.read)) {\n findings.push(\n err(\n `review section \"${section.id}\" reads ${section.read}, which integration \"${section.integration}\" does not declare.`,\n 'Add the operation to the integration in ekanos.json, or read another one.',\n project.entry,\n ),\n );\n } else if (\n contractOps.get(section.integration)?.get(section.read)?.effect !== 'read'\n ) {\n findings.push(\n err(\n `review section \"${section.id}\" reads ${section.read}, which is not a read operation.`,\n 'The review queue is reads-only; writes belong behind a workflow approval.',\n project.entry,\n ),\n );\n }\n }\n\n const effectOf = effectLookup(pinned);\n for (const template of definition.workflows) {\n const where = `workflow \"${template.id}\"`;\n let steps: ReturnType<typeof template.build>;\n try {\n steps = template.build();\n } catch (error) {\n findings.push(\n err(\n `${where}: build() threw: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the template.',\n project.entry,\n ),\n );\n continue;\n }\n\n let stepsOk = true;\n for (const step of steps) {\n if (step.type !== 'tool' || !step.integration || !step.op) continue;\n if (!checkIntegration(step.integration, `${where} step \"${step.id}\"`)) {\n stepsOk = false;\n } else if (!declared.get(step.integration)?.has(step.op)) {\n stepsOk = false;\n findings.push(\n err(\n `${where} step \"${step.id}\" calls ${step.integration} ${step.op}, which the integration does not declare.`,\n 'Add the operation to the integration\\'s \"operations\" in ekanos.json (ekanos add --ops).',\n project.entry,\n ),\n );\n }\n }\n if (!stepsOk) continue;\n\n let dsl: unknown;\n try {\n dsl = loaded.serializeWorkflowTemplate(template, { effectOf });\n } catch (error) {\n findings.push(\n err(\n `${where}: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the template.',\n project.entry,\n ),\n );\n continue;\n }\n const parsed = WorkflowTemplateSchema.safeParse(dsl);\n if (!parsed.success) {\n findings.push(\n err(\n `${where} does not serialize to a valid workflow: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`,\n 'Fix the step definitions.',\n project.entry,\n ),\n );\n continue;\n }\n for (const problem of workflowTemplateProblems(parsed.data)) {\n findings.push(\n err(`${where}: ${problem}.`, 'Fix the template.', project.entry),\n );\n }\n\n const gated = new Set<string>();\n const preds = new Map<string, string[]>();\n for (const e of parsed.data.edges)\n preds.set(e.to, [...(preds.get(e.to) ?? []), e.from]);\n const hasGate = (id: string, seen = new Set<string>()): boolean => {\n if (seen.has(id)) return false;\n seen.add(id);\n\n return (preds.get(id) ?? []).some(\n (p) =>\n parsed.data.steps.find((s) => s.id === p)?.type === 'approval_gate' ||\n hasGate(p, seen),\n );\n };\n for (const s of parsed.data.steps) {\n if (s.type === 'fusion_tool' && s.effect === 'write' && !hasGate(s.id)) {\n gated.add(s.id);\n }\n }\n for (const id of gated) {\n findings.push(\n warn(\n `${where}: write step \"${id}\" has no approval gate upstream.`,\n 'Put step.approval(...) before a write so a person confirms it.',\n project.entry,\n ),\n );\n }\n\n workflows.push(parsed.data);\n }\n\n return { findings, workflows };\n}\n\n// ─── fixtures ───────────────────────────────────────────────────────────────\n\nconst FIXTURE_NAME = /^(tool|action)-(.+)\\.(default|empty|error)\\.json$/;\n\n/** `fixtures/<integration>/<kind>-<name>.<variant>.json` must satisfy the pinned output schema. */\nexport function checkFixtures(\n project: ExperienceProject,\n pinned: readonly PinnedIntegration[],\n): Finding[] {\n const findings: Finding[] = [];\n const root = path.join(project.dir, 'fixtures');\n if (!fs.existsSync(root)) return findings;\n\n for (const { use, contract } of pinned) {\n const dir = path.join(root, use.name);\n if (!fs.existsSync(dir)) continue;\n for (const name of fs.readdirSync(dir)) {\n const rel = `fixtures/${use.name}/${name}`;\n const match = FIXTURE_NAME.exec(name);\n if (!match) {\n findings.push(\n warn(\n `${rel} is not <kind>-<name>.<default|empty|error>.json.`,\n 'Rename it so the harness can find it.',\n rel,\n ),\n );\n continue;\n }\n const [, kind, op, variant] = match;\n if (variant === 'error') continue;\n const operation = contract.operations.find(\n (o) => o.kind === kind && o.name === op,\n );\n if (!operation) {\n findings.push(\n err(\n `${rel}: ${use.name}@${use.version} has no ${kind}/${op}.`,\n 'Delete the fixture or fix its name.',\n rel,\n ),\n );\n continue;\n }\n if (operation.opaque.output) continue;\n let value: unknown;\n try {\n value = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));\n } catch {\n findings.push(err(`${rel} is not valid JSON.`, 'Fix the syntax.', rel));\n continue;\n }\n const problems = validateAgainstSchema(operation.output, value);\n if (problems.length > 0) {\n findings.push(\n err(\n `${rel} violates the pinned output schema: ${problems[0]}`,\n 'A fixture must satisfy the contract, or the harness would show data the integration can never return.',\n rel,\n ),\n );\n }\n }\n }\n\n return findings;\n}\n\n// ─── manifest ───────────────────────────────────────────────────────────────\n\nexport function buildManifest(input: {\n project: ExperienceProject;\n definition: LoadedExperienceDefinition;\n pinned: readonly PinnedIntegration[];\n workflows: readonly WorkflowTemplate[];\n version: string;\n sdk: string;\n}): ExperienceManifest {\n const { project, definition, pinned } = input;\n const body = {\n schemaVersion: 1 as const,\n slug: project.slug,\n form: 'composed' as const,\n version: input.version,\n sdk: input.sdk,\n integrations: project.integrations,\n contractRefs: pinned.map(({ use, contract }) => ({\n integration: use.name,\n version: use.version,\n contractVersion: contract.contractVersion,\n digest: contract.digest,\n })),\n ...(definition.sidebar ? { sidebar: definition.sidebar } : {}),\n widgets: definition.widgets.map((w) => ({\n id: w.id,\n title: w.title,\n integrations: [...w.integrations],\n ...(w.width ? { width: w.width } : {}),\n ...(w.isCollapsible === undefined\n ? {}\n : { isCollapsible: w.isCollapsible }),\n ...(w.aiFooterEnabled === undefined\n ? {}\n : { aiFooterEnabled: w.aiFooterEnabled }),\n })),\n pages: definition.pages.map((p) => ({\n id: p.id,\n path: p.path,\n title: p.title,\n integrations: [...p.integrations],\n })),\n workflowTemplates: [...input.workflows],\n ...(definition.reviewQueue\n ? {\n reviewQueue: {\n readsOnly: true as const,\n sections: definition.reviewQueue.sections.map((s) => ({\n id: s.id,\n integration: s.integration,\n read: s.read,\n })),\n },\n }\n : {}),\n };\n\n return {\n ...body,\n digest: `sha256:${sha256Hex(canonicalJson(body))}`,\n };\n}\n"]}
1
+ {"version":3,"file":"definition-checks.js","sourceRoot":"","sources":["../../src/experience/definition-checks.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type ExperienceManifest,\n type ExperienceSurfaces,\n ExperienceSurfacesSchema,\n type Finding,\n type WorkflowTemplate,\n WorkflowTemplateSchema,\n EKANOS_CHECK_STAGE_IDS,\n buildExperienceSurfaces,\n canonicalJson,\n sha256Hex,\n validateAgainstSchema,\n workflowTemplateProblems,\n} from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type {\n LoadedExperienceDefinition,\n LoadedExperienceModule,\n} from './definition-shape';\nimport { declaredOperationKeys } from './integrations-codegen';\nimport { type PinnedIntegration, effectLookup } from './pins';\nimport type { ExperienceProject } from './project';\n\nexport const DEFINITION_CHECK = 'checks/experience-definition';\nexport const SURFACES_CHECK = EKANOS_CHECK_STAGE_IDS.experienceSurfaces;\n\nconst err = (message: string, hint: string, file?: string): Finding => ({\n check: DEFINITION_CHECK,\n severity: 'error',\n message,\n hint,\n ...(file ? { file } : {}),\n});\n\nconst warn = (message: string, hint: string, file?: string): Finding => ({\n check: DEFINITION_CHECK,\n severity: 'warn',\n message,\n hint,\n ...(file ? { file } : {}),\n});\n\nexport interface DefinitionAnalysis {\n findings: Finding[];\n workflows: WorkflowTemplate[];\n}\n\n/**\n * Cross-check the loaded definition against `ekanos.json` and the pinned\n * contracts: every integration a widget, page, review section or workflow step names\n * must be declared, every operation must be in that integration's allowlist, the\n * review queue reads only, and each template serializes to the engine's DSL.\n */\nexport function analyzeDefinition(\n project: ExperienceProject,\n loaded: LoadedExperienceModule,\n pinned: readonly PinnedIntegration[],\n): DefinitionAnalysis {\n const { definition } = loaded;\n const findings: Finding[] = [];\n const workflows: WorkflowTemplate[] = [];\n const declaredIntegrations = new Map(\n project.integrations.map((u) => [u.name, u]),\n );\n const declared = new Map(\n project.integrations.map((u) => [\n u.name,\n new Set(declaredOperationKeys(u)),\n ]),\n );\n const contractOps = new Map(\n pinned.map(({ use, contract }) => [\n use.name,\n new Map(contract.operations.map((op) => [`${op.kind}/${op.name}`, op])),\n ]),\n );\n\n if (definition.slug !== project.slug) {\n findings.push(\n err(\n `the definition's slug \"${definition.slug}\" differs from ekanos.json \"${project.slug}\".`,\n 'Make them equal.',\n project.entry,\n ),\n );\n }\n\n const checkIntegration = (integration: string, owner: string) => {\n if (!declaredIntegrations.has(integration)) {\n findings.push(\n err(\n `${owner} uses integration \"${integration}\", which ekanos.json \"integrations\" does not declare.`,\n `Add it with \"ekanos add ${integration}@<version>\".`,\n project.entry,\n ),\n );\n\n return false;\n }\n\n return true;\n };\n\n for (const widget of definition.widgets) {\n for (const integration of widget.integrations)\n checkIntegration(integration, `widget \"${widget.id}\"`);\n }\n for (const page of definition.pages) {\n for (const integration of page.integrations ?? [])\n checkIntegration(integration, `page \"${page.id}\"`);\n }\n\n findings.push(...checkSurfaces(project, definition));\n\n for (const section of definition.reviewQueue?.sections ?? []) {\n if (\n !checkIntegration(section.integration, `review section \"${section.id}\"`)\n )\n continue;\n if (!declared.get(section.integration)?.has(section.read)) {\n findings.push(\n err(\n `review section \"${section.id}\" reads ${section.read}, which integration \"${section.integration}\" does not declare.`,\n 'Add the operation to the integration in ekanos.json, or read another one.',\n project.entry,\n ),\n );\n } else if (\n contractOps.get(section.integration)?.get(section.read)?.effect !== 'read'\n ) {\n findings.push(\n err(\n `review section \"${section.id}\" reads ${section.read}, which is not a read operation.`,\n 'The review queue is reads-only; writes belong behind a workflow approval.',\n project.entry,\n ),\n );\n }\n }\n\n const effectOf = effectLookup(pinned);\n for (const template of definition.workflows) {\n const where = `workflow \"${template.id}\"`;\n let steps: ReturnType<typeof template.build>;\n try {\n steps = template.build();\n } catch (error) {\n findings.push(\n err(\n `${where}: build() threw: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the template.',\n project.entry,\n ),\n );\n continue;\n }\n\n let stepsOk = true;\n for (const step of steps) {\n if (step.type !== 'tool' || !step.integration || !step.op) continue;\n if (!checkIntegration(step.integration, `${where} step \"${step.id}\"`)) {\n stepsOk = false;\n } else if (!declared.get(step.integration)?.has(step.op)) {\n stepsOk = false;\n findings.push(\n err(\n `${where} step \"${step.id}\" calls ${step.integration} ${step.op}, which the integration does not declare.`,\n 'Add the operation to the integration\\'s \"operations\" in ekanos.json (ekanos add --ops).',\n project.entry,\n ),\n );\n }\n }\n if (!stepsOk) continue;\n\n let dsl: unknown;\n try {\n dsl = loaded.serializeWorkflowTemplate(template, { effectOf });\n } catch (error) {\n findings.push(\n err(\n `${where}: ${error instanceof Error ? error.message : String(error)}`,\n 'Fix the template.',\n project.entry,\n ),\n );\n continue;\n }\n const parsed = WorkflowTemplateSchema.safeParse(dsl);\n if (!parsed.success) {\n findings.push(\n err(\n `${where} does not serialize to a valid workflow: ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`,\n 'Fix the step definitions.',\n project.entry,\n ),\n );\n continue;\n }\n for (const problem of workflowTemplateProblems(parsed.data)) {\n findings.push(\n err(`${where}: ${problem}.`, 'Fix the template.', project.entry),\n );\n }\n\n const gated = new Set<string>();\n const preds = new Map<string, string[]>();\n for (const e of parsed.data.edges)\n preds.set(e.to, [...(preds.get(e.to) ?? []), e.from]);\n const hasGate = (id: string, seen = new Set<string>()): boolean => {\n if (seen.has(id)) return false;\n seen.add(id);\n\n return (preds.get(id) ?? []).some(\n (p) =>\n parsed.data.steps.find((s) => s.id === p)?.type === 'approval_gate' ||\n hasGate(p, seen),\n );\n };\n for (const s of parsed.data.steps) {\n if (s.type === 'fusion_tool' && s.effect === 'write' && !hasGate(s.id)) {\n gated.add(s.id);\n }\n }\n for (const id of gated) {\n findings.push(\n warn(\n `${where}: write step \"${id}\" has no approval gate upstream.`,\n 'Put step.approval(...) before a write so a person confirms it.',\n project.entry,\n ),\n );\n }\n\n workflows.push(parsed.data);\n }\n\n return { findings, workflows };\n}\n\n// ─── surfaces ───────────────────────────────────────────────────────────────\n\n/**\n * The surfaces contract: page kinds, canvas widgets, routes and the suggested\n * layout must be well formed and refer only to what the experience declares.\n * The same rules `defineExperience` enforces, re-run on the loaded definition\n * so a definition built by an older SDK, or by hand, cannot slip past.\n */\nexport function checkSurfaces(\n project: ExperienceProject,\n definition: LoadedExperienceDefinition,\n): Finding[] {\n const built = buildSurfaces(project, definition);\n const parsed = ExperienceSurfacesSchema.safeParse(built);\n if (parsed.success) return [];\n\n return parsed.error.issues.map((issue) => ({\n check: SURFACES_CHECK,\n severity: 'error' as const,\n message: `${issue.path.length > 0 ? `${issue.path.join('.')}: ` : ''}${issue.message}`,\n hint: 'Fix the page kinds, canvas widgets or suggestedLayout in the experience definition.',\n file: project.entry,\n }));\n}\n\n/** `dist/surfaces.json`: the surfaces and suggested layout as data, no code. */\nexport function buildSurfaces(\n project: ExperienceProject,\n definition: LoadedExperienceDefinition,\n): ExperienceSurfaces {\n return buildExperienceSurfaces(project.slug, definition);\n}\n\n// ─── fixtures ───────────────────────────────────────────────────────────────\n\nconst FIXTURE_NAME = /^(tool|action)-(.+)\\.(default|empty|error)\\.json$/;\n\n/** `fixtures/<integration>/<kind>-<name>.<variant>.json` must satisfy the pinned output schema. */\nexport function checkFixtures(\n project: ExperienceProject,\n pinned: readonly PinnedIntegration[],\n): Finding[] {\n const findings: Finding[] = [];\n const root = path.join(project.dir, 'fixtures');\n if (!fs.existsSync(root)) return findings;\n\n for (const { use, contract } of pinned) {\n const dir = path.join(root, use.name);\n if (!fs.existsSync(dir)) continue;\n for (const name of fs.readdirSync(dir)) {\n const rel = `fixtures/${use.name}/${name}`;\n const match = FIXTURE_NAME.exec(name);\n if (!match) {\n findings.push(\n warn(\n `${rel} is not <kind>-<name>.<default|empty|error>.json.`,\n 'Rename it so the harness can find it.',\n rel,\n ),\n );\n continue;\n }\n const [, kind, op, variant] = match;\n if (variant === 'error') continue;\n const operation = contract.operations.find(\n (o) => o.kind === kind && o.name === op,\n );\n if (!operation) {\n findings.push(\n err(\n `${rel}: ${use.name}@${use.version} has no ${kind}/${op}.`,\n 'Delete the fixture or fix its name.',\n rel,\n ),\n );\n continue;\n }\n if (operation.opaque.output) continue;\n let value: unknown;\n try {\n value = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));\n } catch {\n findings.push(err(`${rel} is not valid JSON.`, 'Fix the syntax.', rel));\n continue;\n }\n const problems = validateAgainstSchema(operation.output, value);\n if (problems.length > 0) {\n findings.push(\n err(\n `${rel} violates the pinned output schema: ${problems[0]}`,\n 'A fixture must satisfy the contract, or the harness would show data the integration can never return.',\n rel,\n ),\n );\n }\n }\n }\n\n return findings;\n}\n\n// ─── manifest ───────────────────────────────────────────────────────────────\n\nexport function buildManifest(input: {\n project: ExperienceProject;\n definition: LoadedExperienceDefinition;\n pinned: readonly PinnedIntegration[];\n workflows: readonly WorkflowTemplate[];\n version: string;\n sdk: string;\n surfaces: ExperienceSurfaces;\n}): ExperienceManifest {\n const { project, definition, pinned } = input;\n const body = {\n schemaVersion: 1 as const,\n slug: project.slug,\n form: 'composed' as const,\n version: input.version,\n sdk: input.sdk,\n integrations: project.integrations,\n contractRefs: pinned.map(({ use, contract }) => ({\n integration: use.name,\n version: use.version,\n contractVersion: contract.contractVersion,\n digest: contract.digest,\n })),\n ...(definition.sidebar ? { sidebar: definition.sidebar } : {}),\n widgets: definition.widgets.map((w) => ({\n id: w.id,\n title: w.title,\n integrations: [...w.integrations],\n ...(w.width ? { width: w.width } : {}),\n ...(w.isCollapsible === undefined\n ? {}\n : { isCollapsible: w.isCollapsible }),\n ...(w.aiFooterEnabled === undefined\n ? {}\n : { aiFooterEnabled: w.aiFooterEnabled }),\n })),\n pages: definition.pages.map((p) => ({\n id: p.id,\n ...(p.path === undefined ? {} : { path: p.path }),\n title: p.title,\n integrations: [...(p.integrations ?? [])],\n ...(p.kind === undefined ? {} : { kind: p.kind }),\n ...(p.widgets === undefined ? {} : { widgets: [...p.widgets] }),\n ...(p.routes && p.routes.length > 0\n ? { routes: p.routes.map((r) => ({ path: r.path, title: r.title })) }\n : {}),\n })),\n surfacesDigest: input.surfaces.digest,\n workflowTemplates: [...input.workflows],\n ...(definition.reviewQueue\n ? {\n reviewQueue: {\n readsOnly: true as const,\n sections: definition.reviewQueue.sections.map((s) => ({\n id: s.id,\n integration: s.integration,\n read: s.read,\n })),\n },\n }\n : {}),\n };\n\n return {\n ...body,\n digest: `sha256:${sha256Hex(canonicalJson(body))}`,\n };\n}\n"]}
@@ -35,10 +35,28 @@ export interface LoadedExperienceDefinition {
35
35
  }>;
36
36
  pages: ReadonlyArray<{
37
37
  id: string;
38
- path: string;
38
+ /** The SDK sets it; a definition from before page kinds has none (custom). */
39
+ kind?: 'overview' | 'canvas' | 'custom';
40
+ path?: string;
39
41
  title: string;
40
- integrations: readonly string[];
42
+ integrations?: readonly string[];
43
+ widgets?: readonly string[];
44
+ routes?: ReadonlyArray<{
45
+ path: string;
46
+ title: string;
47
+ }>;
41
48
  }>;
49
+ suggestedLayout?: {
50
+ workspaces: ReadonlyArray<{
51
+ slug: string;
52
+ name: string;
53
+ icon?: string;
54
+ items: ReadonlyArray<{
55
+ ref: string;
56
+ audiences?: readonly string[];
57
+ }>;
58
+ }>;
59
+ };
42
60
  workflows: readonly LoadedWorkflowTemplate[];
43
61
  reviewQueue?: {
44
62
  readsOnly: true;
@@ -1 +1 @@
1
- {"version":3,"file":"definition-shape.js","sourceRoot":"","sources":["../../src/experience/definition-shape.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The structural slice of an `ExperienceDefinition` the CLI reads. The CLI does\n * not depend on `@ekanos/sdk`: the definition is produced by whichever SDK the\n * project installed, and anything the CLI needs computed (workflow\n * serialization) is obtained from that same SDK at load time.\n */\nexport interface LoadedWorkflowTemplate {\n id: string;\n name: string;\n build(): ReadonlyArray<{\n type: string;\n id: string;\n integration?: string;\n op?: string;\n }>;\n}\n\nexport interface LoadedExperienceDefinition {\n kind: 'experience';\n slug: string;\n name: string;\n description?: string;\n sidebar?: { section: string; icon: string; label: string };\n widgets: ReadonlyArray<{\n id: string;\n title: string;\n integrations: readonly string[];\n width?: string;\n isCollapsible?: boolean;\n isPinnable?: boolean;\n aiFooterEnabled?: boolean;\n }>;\n pages: ReadonlyArray<{\n id: string;\n path: string;\n title: string;\n integrations: readonly string[];\n }>;\n workflows: readonly LoadedWorkflowTemplate[];\n reviewQueue?: {\n readsOnly: true;\n sections: ReadonlyArray<{ id: string; integration: string; read: string }>;\n };\n}\n\nexport interface SerializedWorkflowOptions {\n effectOf: (integration: string, op: string) => 'read' | 'write' | undefined;\n}\n\nexport interface LoadedExperienceModule {\n definition: LoadedExperienceDefinition;\n /** The project's own SDK serializer, so the output matches the SDK that authored it. */\n serializeWorkflowTemplate(\n template: LoadedWorkflowTemplate,\n options: SerializedWorkflowOptions,\n ): unknown;\n slotsUsedBy(definition: LoadedExperienceDefinition): string[];\n}\n"]}
1
+ {"version":3,"file":"definition-shape.js","sourceRoot":"","sources":["../../src/experience/definition-shape.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The structural slice of an `ExperienceDefinition` the CLI reads. The CLI does\n * not depend on `@ekanos/sdk`: the definition is produced by whichever SDK the\n * project installed, and anything the CLI needs computed (workflow\n * serialization) is obtained from that same SDK at load time.\n */\nexport interface LoadedWorkflowTemplate {\n id: string;\n name: string;\n build(): ReadonlyArray<{\n type: string;\n id: string;\n integration?: string;\n op?: string;\n }>;\n}\n\nexport interface LoadedExperienceDefinition {\n kind: 'experience';\n slug: string;\n name: string;\n description?: string;\n sidebar?: { section: string; icon: string; label: string };\n widgets: ReadonlyArray<{\n id: string;\n title: string;\n integrations: readonly string[];\n width?: string;\n isCollapsible?: boolean;\n isPinnable?: boolean;\n aiFooterEnabled?: boolean;\n }>;\n pages: ReadonlyArray<{\n id: string;\n /** The SDK sets it; a definition from before page kinds has none (custom). */\n kind?: 'overview' | 'canvas' | 'custom';\n path?: string;\n title: string;\n integrations?: readonly string[];\n widgets?: readonly string[];\n routes?: ReadonlyArray<{ path: string; title: string }>;\n }>;\n suggestedLayout?: {\n workspaces: ReadonlyArray<{\n slug: string;\n name: string;\n icon?: string;\n items: ReadonlyArray<{ ref: string; audiences?: readonly string[] }>;\n }>;\n };\n workflows: readonly LoadedWorkflowTemplate[];\n reviewQueue?: {\n readsOnly: true;\n sections: ReadonlyArray<{ id: string; integration: string; read: string }>;\n };\n}\n\nexport interface SerializedWorkflowOptions {\n effectOf: (integration: string, op: string) => 'read' | 'write' | undefined;\n}\n\nexport interface LoadedExperienceModule {\n definition: LoadedExperienceDefinition;\n /** The project's own SDK serializer, so the output matches the SDK that authored it. */\n serializeWorkflowTemplate(\n template: LoadedWorkflowTemplate,\n options: SerializedWorkflowOptions,\n ): unknown;\n slotsUsedBy(definition: LoadedExperienceDefinition): string[];\n}\n"]}
@@ -95,9 +95,15 @@ export function readExperienceProject(dir) {
95
95
  raw: record,
96
96
  };
97
97
  }
98
- /** Operations as canonical `<integration>@<version>/<kind>/<name>` ids, for lookups against a contract. */
98
+ /**
99
+ * Operations as canonical `<slug>@<version>/<kind>/<name>` ids, for lookups
100
+ * against a contract. A contract's ids carry the bare slug even when the
101
+ * package is named under a namespace, so a `vastly/news` entry canonicalises to
102
+ * `news@0.1/...`.
103
+ */
99
104
  function withCanonicalOperations(entry) {
100
- return Object.assign(Object.assign({}, entry), { operations: entry.operations.map((id) => canonicalOperationId(entry, id)) });
105
+ const slug = entry.name.slice(entry.name.indexOf('/') + 1);
106
+ return Object.assign(Object.assign({}, entry), { operations: entry.operations.map((id) => canonicalOperationId({ name: slug, version: entry.version }, id)) });
101
107
  }
102
108
  /** 2-space JSON with a trailing newline: the format every written file uses. */
103
109
  export function formatJson(value) {