@vertesia/studio-utils 1.5.0-dev.20260910.052912Z → 1.5.1

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 (40) hide show
  1. package/lib/conditions/index.d.ts +1 -0
  2. package/lib/conditions/index.d.ts.map +1 -1
  3. package/lib/conditions/index.js +1 -0
  4. package/lib/conditions/index.js.map +1 -1
  5. package/lib/conditions/principal-context.d.ts +55 -0
  6. package/lib/conditions/principal-context.d.ts.map +1 -0
  7. package/lib/conditions/principal-context.js +81 -0
  8. package/lib/conditions/principal-context.js.map +1 -0
  9. package/lib/index.d.ts +2 -1
  10. package/lib/index.d.ts.map +1 -1
  11. package/lib/index.js +4 -1
  12. package/lib/index.js.map +1 -1
  13. package/lib/prompts/extract-vars.d.ts +44 -12
  14. package/lib/prompts/extract-vars.d.ts.map +1 -1
  15. package/lib/prompts/extract-vars.js +165 -75
  16. package/lib/prompts/extract-vars.js.map +1 -1
  17. package/lib/prompts/render.d.ts +7 -9
  18. package/lib/prompts/render.d.ts.map +1 -1
  19. package/lib/prompts/render.js +12 -15
  20. package/lib/prompts/render.js.map +1 -1
  21. package/lib/prompts/validate.d.ts +8 -2
  22. package/lib/prompts/validate.d.ts.map +1 -1
  23. package/lib/prompts/validate.js +87 -50
  24. package/lib/prompts/validate.js.map +1 -1
  25. package/lib/roles/system.d.ts.map +1 -1
  26. package/lib/roles/system.js +15 -0
  27. package/lib/roles/system.js.map +1 -1
  28. package/lib/vertesia-studio-utils.js +2 -1
  29. package/lib/vertesia-studio-utils.js.map +1 -1
  30. package/package.json +6 -5
  31. package/src/conditions/index.ts +6 -0
  32. package/src/conditions/principal-context.ts +122 -0
  33. package/src/index.ts +20 -1
  34. package/src/prompts/extract-vars.ts +212 -68
  35. package/src/prompts/render.test.ts +13 -2
  36. package/src/prompts/render.ts +24 -15
  37. package/src/prompts/validate.test.ts +115 -0
  38. package/src/prompts/validate.ts +113 -56
  39. package/src/roles/index.test.ts +9 -9
  40. package/src/roles/system.ts +16 -0
@@ -1,13 +1,25 @@
1
1
  import type { JSONObject } from '@llumiverse/common';
2
2
  import { type JSONSchema, TemplateType } from '@vertesia/common';
3
- import { getFreeVariables, renderJsTemplate } from '@vertesia/jst';
4
- import { extractHandlebarsVariables } from './extract-vars.js';
3
+ import {
4
+ describeTemplateSystemVariables,
5
+ getFreeVariables,
6
+ isTemplateSystemVariable,
7
+ JST_TEMPLATE_GLOBALS,
8
+ renderJsTemplate,
9
+ TEMPLATE_SYSTEM_VARIABLES,
10
+ withTemplateSystemVariables,
11
+ } from '@vertesia/jst';
12
+ import { analyzeHandlebarsTemplate } from './extract-vars.js';
5
13
  import { generateMockData } from './mock-data.js';
6
14
  import { executeHandlebars } from './render.js';
7
15
 
8
16
  export type PromptValidationIssueType =
9
17
  | 'undeclared_template_variable'
10
18
  | 'unused_schema_variable'
19
+ | 'reserved_variable_declared'
20
+ | 'system_variable_property_access'
21
+ | 'helper_used_as_value'
22
+ | 'helper_missing_arguments'
11
23
  | 'handlebars_render_error'
12
24
  | 'jst_unsafe_construct'
13
25
  | 'jst_render_error';
@@ -43,16 +55,6 @@ export interface PromptValidationInput {
43
55
  inputSchema?: JSONSchema;
44
56
  }
45
57
 
46
- // JST's renderJsTemplate auto-adds `_` (helpers object) and runtime injects `Set` and `Array`
47
- // — treat them as globals so they don't appear as free vars in user templates.
48
- // Globals always available to JST templates regardless of schema:
49
- // - `_`, `Array`, `Set`: runtime-injected by `renderJsTemplate` (jst library)
50
- // - `_model`: runtime-injected by the studio-server executor as `{ ..._model: run.modelId }`
51
- // (see ExecutionRequest.ts:313 and executor/rendering/template.ts:13)
52
- // Keeping these in sync with `renderTemplate` in ./render.ts so a JST template that runs in
53
- // production also passes the validator.
54
- const JST_AUTO_GLOBALS = ['_', 'Array', 'Set', '_model'];
55
-
56
58
  function countSeverities(issues: PromptValidationIssue[]): { error_count: number; warning_count: number } {
57
59
  let error_count = 0;
58
60
  let warning_count = 0;
@@ -66,42 +68,110 @@ function countSeverities(issues: PromptValidationIssue[]): { error_count: number
66
68
  return { error_count, warning_count };
67
69
  }
68
70
 
69
- function validateHandlebarsPrompt(content: string, inputSchema?: JSONSchema): PromptValidationIssue[] {
70
- const issues: PromptValidationIssue[] = [];
71
- const usedVars = extractHandlebarsVariables(content);
72
- const declaredVars = new Set<string>(inputSchema?.properties ? Object.keys(inputSchema.properties) : []);
71
+ /** Mock data for the render smoke test: schema-shaped input plus the runtime system values. */
72
+ function buildMockInput(inputSchema: JSONSchema): Record<string, unknown> {
73
+ const mockData = generateMockData(inputSchema);
74
+ const mockObject: JSONObject =
75
+ typeof mockData === 'object' && mockData !== null && !Array.isArray(mockData) ? (mockData as JSONObject) : {};
76
+ return withTemplateSystemVariables(mockObject, { model: 'validation-model' });
77
+ }
73
78
 
74
- for (const used of usedVars) {
75
- if (!declaredVars.has(used)) {
79
+ /**
80
+ * Issues shared by both template languages: reserved names declared in the schema, and
81
+ * unused schema properties. `usedVars` holds the input variables the template reads.
82
+ */
83
+ function checkDeclarations(
84
+ declaredVars: Set<string>,
85
+ usedVars: Set<string>,
86
+ usageHint: (name: string) => string,
87
+ ): PromptValidationIssue[] {
88
+ const issues: PromptValidationIssue[] = [];
89
+ for (const declared of declaredVars) {
90
+ if (isTemplateSystemVariable(declared)) {
76
91
  issues.push({
77
- type: 'undeclared_template_variable',
78
- severity: 'error',
79
- variable: used,
80
- message: `Template references variable '{{${used}}}' but it is not declared in input_schema.properties. Add '${used}' to the schema with an appropriate type.`,
92
+ type: 'reserved_variable_declared',
93
+ severity: 'warning',
94
+ variable: declared,
95
+ message: `Schema declares '${declared}', a system variable the runtime supplies and overrides. Remove it from input_schema.`,
81
96
  });
82
- }
83
- }
84
-
85
- for (const declared of declaredVars) {
86
- if (!usedVars.has(declared)) {
97
+ } else if (!usedVars.has(declared)) {
87
98
  issues.push({
88
99
  type: 'unused_schema_variable',
89
100
  severity: 'warning',
90
101
  variable: declared,
91
- message: `Schema declares property '${declared}' but the template never references it. Remove it from input_schema or use it via {{${declared}}}.`,
102
+ message: `Schema declares property '${declared}' but the template never references it. Remove it from input_schema or ${usageHint(declared)}.`,
92
103
  });
93
104
  }
94
105
  }
106
+ return issues;
107
+ }
108
+
109
+ function undeclaredVariableIssue(name: string, where: string): PromptValidationIssue {
110
+ return {
111
+ type: 'undeclared_template_variable',
112
+ severity: 'error',
113
+ variable: name,
114
+ message:
115
+ `Template reads variable '${name}' ${where} but it is not declared in input_schema.properties. ` +
116
+ `Add '${name}' to the schema with an appropriate type. ` +
117
+ `System variables need no declaration: ${describeTemplateSystemVariables()}.`,
118
+ };
119
+ }
120
+
121
+ function validateHandlebarsPrompt(content: string, inputSchema?: JSONSchema): PromptValidationIssue[] {
122
+ const issues: PromptValidationIssue[] = [];
123
+ const analysis = analyzeHandlebarsTemplate(content);
124
+ const declaredVars = new Set<string>(inputSchema?.properties ? Object.keys(inputSchema.properties) : []);
125
+ const usedVars = new Set<string>();
126
+ const reported = new Set<string>();
127
+
128
+ for (const ref of analysis?.references ?? []) {
129
+ if (isTemplateSystemVariable(ref.name)) {
130
+ const variable = TEMPLATE_SYSTEM_VARIABLES.find((v) => v.name === ref.name);
131
+ if (ref.hasPath && variable?.type === 'string' && !reported.has(`path:${ref.expression}`)) {
132
+ reported.add(`path:${ref.expression}`);
133
+ issues.push({
134
+ type: 'system_variable_property_access',
135
+ severity: 'error',
136
+ variable: ref.name,
137
+ message: `'${ref.expression}' in ${ref.tag} reads a property of system variable '${ref.name}', which is a string (${variable.description}). Use '${ref.name}' directly.`,
138
+ });
139
+ }
140
+ continue;
141
+ }
142
+ usedVars.add(ref.name);
143
+ if (!declaredVars.has(ref.name) && !reported.has(ref.name)) {
144
+ reported.add(ref.name);
145
+ issues.push(undeclaredVariableIssue(ref.name, `in ${ref.tag}`));
146
+ }
147
+ }
148
+
149
+ for (const misuse of analysis?.helperMisuses ?? []) {
150
+ issues.push(
151
+ misuse.problem === 'used_as_value'
152
+ ? {
153
+ type: 'helper_used_as_value',
154
+ severity: 'error',
155
+ variable: misuse.helper,
156
+ message: `'${misuse.helper}' in ${misuse.tag} is a helper, but as an argument Handlebars reads it as data, which is empty. Call it as a subexpression: (${misuse.helper} ...).`,
157
+ }
158
+ : {
159
+ type: 'helper_missing_arguments',
160
+ severity: 'error',
161
+ variable: misuse.helper,
162
+ message: `${misuse.tag} calls helper '${misuse.helper}' without arguments. Pass the value it operates on, e.g. {{${misuse.helper} value}}.`,
163
+ },
164
+ );
165
+ }
166
+
167
+ issues.push(...checkDeclarations(declaredVars, usedVars, (name) => `use it via {{${name}}}`));
95
168
 
96
169
  // Render-time smoke test — always runs so syntax errors and failing helper calls are
97
170
  // surfaced even when undeclared-variable errors are already in the list. Handlebars renders
98
171
  // missing vars as empty strings (non-strict by default), so the render check does NOT echo
99
172
  // the var errors — anything it reports is a distinct template problem worth showing.
100
173
  const renderSchema = inputSchema ?? ({} as JSONSchema);
101
- const mockData = generateMockData(renderSchema);
102
- const mockObject: JSONObject =
103
- typeof mockData === 'object' && mockData !== null && !Array.isArray(mockData) ? (mockData as JSONObject) : {};
104
- const renderResult = executeHandlebars(content, renderSchema, mockObject);
174
+ const renderResult = executeHandlebars(content, renderSchema, buildMockInput(renderSchema) as JSONObject);
105
175
  if (!renderResult.success) {
106
176
  issues.push({
107
177
  type: 'handlebars_render_error',
@@ -120,7 +190,7 @@ function validateJstPrompt(content: string, inputSchema?: JSONSchema): PromptVal
120
190
  let referenced: Set<string>;
121
191
  try {
122
192
  const result = getFreeVariables(content, {
123
- globals: JST_AUTO_GLOBALS,
193
+ globals: [...JST_TEMPLATE_GLOBALS],
124
194
  acorn: { allowReturnOutsideFunction: true, locations: true },
125
195
  });
126
196
  referenced = result.vars;
@@ -143,38 +213,19 @@ function validateJstPrompt(content: string, inputSchema?: JSONSchema): PromptVal
143
213
 
144
214
  for (const used of referenced) {
145
215
  if (!declaredVars.has(used)) {
146
- issues.push({
147
- type: 'undeclared_template_variable',
148
- severity: 'error',
149
- variable: used,
150
- message: `Template references variable '${used}' but it is not declared in input_schema.properties. Add '${used}' to the schema with an appropriate type.`,
151
- });
216
+ issues.push(undeclaredVariableIssue(used, 'in the template'));
152
217
  }
153
218
  }
154
219
 
155
- for (const declared of declaredVars) {
156
- if (!referenced.has(declared)) {
157
- issues.push({
158
- type: 'unused_schema_variable',
159
- severity: 'warning',
160
- variable: declared,
161
- message: `Schema declares property '${declared}' but the template never references it. Remove it from input_schema or use it in the template.`,
162
- });
163
- }
164
- }
220
+ issues.push(...checkDeclarations(declaredVars, referenced, () => 'use it in the template'));
165
221
 
166
222
  // Render-time smoke test — only if there are no blocking errors so far, otherwise
167
223
  // the failure mode would just echo what we already reported.
168
224
  const blockingSoFar = issues.some((i) => i.severity === 'error');
169
225
  if (!blockingSoFar) {
170
226
  const renderSchema = inputSchema ?? ({} as JSONSchema);
171
- const mockData = generateMockData(renderSchema);
172
- const mockObject: JSONObject =
173
- typeof mockData === 'object' && mockData !== null && !Array.isArray(mockData)
174
- ? (mockData as JSONObject)
175
- : {};
176
227
  try {
177
- renderJsTemplate(content, [...declaredVars], mockObject);
228
+ renderJsTemplate(content, [...declaredVars], buildMockInput(renderSchema));
178
229
  } catch (renderError) {
179
230
  issues.push({
180
231
  type: 'jst_render_error',
@@ -192,9 +243,15 @@ function validateJstPrompt(content: string, inputSchema?: JSONSchema): PromptVal
192
243
  *
193
244
  * For `handlebars` and `jst` templates, the following checks are performed:
194
245
  * 1. Every variable referenced in the template must be declared as a top-level property
195
- * in `inputSchema.properties` (else → `undeclared_template_variable` error).
246
+ * in `inputSchema.properties` (else → `undeclared_template_variable` error). System variables
247
+ * (`TEMPLATE_SYSTEM_VARIABLES` in `@vertesia/jst`) are supplied at runtime and need no declaration;
248
+ * declaring one → `reserved_variable_declared` warning, reading a property of one →
249
+ * `system_variable_property_access` error.
196
250
  * 2. Every property declared in `inputSchema.properties` should be referenced by the template
197
251
  * (else → `unused_schema_variable` warning — non-blocking).
252
+ * For Handlebars only: a helper passed as an argument (`{{#if stringify}}`) →
253
+ * `helper_used_as_value` error; a helper called bare without the arguments it needs
254
+ * (`{{stringify}}`) → `helper_missing_arguments` error.
198
255
  * 3. The template must render successfully against schema-derived mock data
199
256
  * (else → `handlebars_render_error` / `jst_render_error` error).
200
257
  * 4. For JST only: unsafe constructs (`with`, `for`, `while`, `import`, class, `this`,
@@ -46,27 +46,27 @@ describe('getRoleByName', () => {
46
46
  describe('listRoles', () => {
47
47
  it('returns every role across all partitions', () => {
48
48
  const roles = listRoles();
49
- // 16 system roles + 3 content roles
50
- expect(roles).toHaveLength(19);
49
+ // 17 system roles + 3 content roles
50
+ expect(roles).toHaveLength(20);
51
51
  });
52
52
 
53
53
  it('lists system roles before content roles (partition registration order)', () => {
54
54
  const roles = listRoles();
55
55
  const systemCount = roles.filter((r) => r.domain === 'system').length;
56
56
  const contentCount = roles.filter((r) => r.domain === 'content').length;
57
- expect(systemCount).toBe(16);
57
+ expect(systemCount).toBe(17);
58
58
  expect(contentCount).toBe(3);
59
59
 
60
- // First 16 are system, next 3 are content
61
- for (let i = 0; i < 16; i++) expect(roles[i].domain).toBe('system');
62
- for (let i = 16; i < 19; i++) expect(roles[i].domain).toBe('content');
60
+ // First 17 are system, next 3 are content
61
+ for (let i = 0; i < 17; i++) expect(roles[i].domain).toBe('system');
62
+ for (let i = 17; i < 20; i++) expect(roles[i].domain).toBe('content');
63
63
  });
64
64
  });
65
65
 
66
66
  describe('listRolesByDomain', () => {
67
67
  it('returns only system roles for "system"', () => {
68
68
  const roles = listRolesByDomain('system');
69
- expect(roles).toHaveLength(16);
69
+ expect(roles).toHaveLength(17);
70
70
  expect(roles.every((r) => r.domain === 'system')).toBe(true);
71
71
  });
72
72
 
@@ -84,7 +84,7 @@ describe('listRolesByDomain', () => {
84
84
  describe('listSystemRoles', () => {
85
85
  it('returns SystemRole instances', () => {
86
86
  const roles = listSystemRoles();
87
- expect(roles).toHaveLength(16);
87
+ expect(roles).toHaveLength(17);
88
88
  expect(roles.every((r) => r instanceof SystemRole)).toBe(true);
89
89
  });
90
90
 
@@ -120,7 +120,7 @@ describe('listAbacRolesForScope', () => {
120
120
  describe('getAllRoleNames', () => {
121
121
  it('returns names of every registered role', () => {
122
122
  const names = getAllRoleNames();
123
- expect(names).toHaveLength(19);
123
+ expect(names).toHaveLength(20);
124
124
  expect(names).toContain('owner');
125
125
  expect(names).toContain('content:reader');
126
126
  });
@@ -19,6 +19,21 @@ class AdminRole extends OrgMemberRole {
19
19
  }
20
20
  }
21
21
 
22
+ /**
23
+ * Full `admin` rights minus Studio UI access. For an account administrator who operates the
24
+ * platform through a custom admin UI and must not reach Vertesia Studio itself.
25
+ *
26
+ * Keep in lockstep with `AdminRole`: `studio_access` is the only intended difference, so a new
27
+ * permission added to the central enum reaches both roles automatically.
28
+ */
29
+ class AppAdminRole extends AdminRole {
30
+ constructor() {
31
+ super();
32
+ this.name = SystemRoles.app_admin;
33
+ this.permissions.delete(Permission.studio_access);
34
+ }
35
+ }
36
+
22
37
  class ManagerRole extends OrgMemberRole {
23
38
  constructor() {
24
39
  super(SystemRoles.manager, Object.values(Permission));
@@ -182,6 +197,7 @@ class ContentSuperAdmin extends DeveloperRole {
182
197
  const systemRoles: Record<SystemRoles, Role> = {
183
198
  [SystemRoles.owner]: new OwnerRole(),
184
199
  [SystemRoles.admin]: new AdminRole(),
200
+ [SystemRoles.app_admin]: new AppAdminRole(),
185
201
  [SystemRoles.manager]: new ManagerRole(),
186
202
  [SystemRoles.developer]: new DeveloperRole(),
187
203
  [SystemRoles.application]: new ApplicationRole(),