@specverse/engines 6.39.3 → 6.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +39 -20
- package/dist/libs/instance-factories/services/templates/prisma/service-generator.js +14 -5
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +10 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +101 -13
- package/dist/realize/index.js.map +1 -1
- package/dist/realize/per-owner-emit.d.ts +101 -0
- package/dist/realize/per-owner-emit.d.ts.map +1 -0
- package/dist/realize/per-owner-emit.js +351 -0
- package/dist/realize/per-owner-emit.js.map +1 -0
- package/dist/realize/per-owner-runner.d.ts +105 -0
- package/dist/realize/per-owner-runner.d.ts.map +1 -0
- package/dist/realize/per-owner-runner.js +336 -0
- package/dist/realize/per-owner-runner.js.map +1 -0
- package/dist/realize/runtime-emitters/library.d.ts.map +1 -1
- package/dist/realize/runtime-emitters/library.js +17 -1
- package/dist/realize/runtime-emitters/library.js.map +1 -1
- package/libs/instance-factories/services/templates/_shared/step-matching.d.ts +39 -0
- package/libs/instance-factories/services/templates/_shared/step-matching.d.ts.map +1 -0
- package/libs/instance-factories/services/templates/_shared/step-matching.js +90 -0
- package/libs/instance-factories/services/templates/_shared/step-matching.js.map +1 -0
- package/libs/instance-factories/services/templates/prisma/__tests__/step-conventions-return.test.ts +124 -0
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +70 -22
- package/libs/instance-factories/services/templates/prisma/service-generator.ts +52 -6
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +33 -1
- package/package.json +1 -1
package/libs/instance-factories/services/templates/prisma/__tests__/step-conventions-return.test.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step-conventions — `return` pattern safety tests.
|
|
3
|
+
*
|
|
4
|
+
* The 2026-05-11 per-owner trial surfaced a defect: when a spec step
|
|
5
|
+
* like `Return logout confirmation` matched the `^return\s+(.+)/i`
|
|
6
|
+
* pattern, the convention emitted `return logout confirmation;` —
|
|
7
|
+
* two bare identifiers after `return`, a parse error. Per-action
|
|
8
|
+
* passed this through because each body parsed in isolation only
|
|
9
|
+
* checked the wrapping body, not the surrounding file. Per-owner
|
|
10
|
+
* caught it because the whole-file parse check failed. Either way,
|
|
11
|
+
* runtime would have broken.
|
|
12
|
+
*
|
|
13
|
+
* Fix: the convention now ONLY emits when the value is a single
|
|
14
|
+
* bareword identifier (or simple property accessor) AND was
|
|
15
|
+
* previously declared in the action's emitted body. Otherwise
|
|
16
|
+
* returns the empty string so the matcher falls through to the AI
|
|
17
|
+
* fallback (LLM authors the return).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest';
|
|
21
|
+
import { STEP_CONVENTIONS } from '../step-conventions.js';
|
|
22
|
+
import type { StepContext } from '../step-conventions.js';
|
|
23
|
+
|
|
24
|
+
function findReturnConvention() {
|
|
25
|
+
const c = STEP_CONVENTIONS.find((x) => x.name === 'return');
|
|
26
|
+
expect(c).toBeDefined();
|
|
27
|
+
return c!;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeCtx(overrides: Partial<StepContext> = {}): StepContext {
|
|
31
|
+
return {
|
|
32
|
+
modelName: 'Order',
|
|
33
|
+
serviceName: 'OrderService',
|
|
34
|
+
operationName: 'createOrder',
|
|
35
|
+
stepNum: 7,
|
|
36
|
+
parameterNames: [],
|
|
37
|
+
declaredVars: new Set<string>(),
|
|
38
|
+
...overrides,
|
|
39
|
+
} as any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("prisma step-convention `return` — only emits parseable TS", () => {
|
|
43
|
+
it("emits the model var for `Return updated order` (model-pattern matches)", () => {
|
|
44
|
+
const convention = findReturnConvention();
|
|
45
|
+
const m = 'Return updated order'.match(convention.pattern)!;
|
|
46
|
+
const call = convention.generateCall(m, makeCtx());
|
|
47
|
+
expect(call).toContain('return order;');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("emits `return <name>;` when the value is a single identifier already declared", () => {
|
|
51
|
+
const convention = findReturnConvention();
|
|
52
|
+
const m = 'Return user'.match(convention.pattern)!;
|
|
53
|
+
const ctx = makeCtx({ declaredVars: new Set(['user']) });
|
|
54
|
+
const call = convention.generateCall(m, ctx);
|
|
55
|
+
expect(call).toContain('return user;');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("emits `return <obj>.<prop>;` for a simple property accessor when head is declared", () => {
|
|
59
|
+
const convention = findReturnConvention();
|
|
60
|
+
const m = 'Return result.payload'.match(convention.pattern)!;
|
|
61
|
+
const ctx = makeCtx({ declaredVars: new Set(['result']) });
|
|
62
|
+
const call = convention.generateCall(m, ctx);
|
|
63
|
+
expect(call).toContain('return result.payload;');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("REGRESSION: prose phrase 'logout confirmation' returns empty (bounces to LLM)", () => {
|
|
67
|
+
// Pre-fix this emitted `return logout confirmation;` — two bare
|
|
68
|
+
// identifiers, parse error. New behaviour: empty string → matcher
|
|
69
|
+
// treats as no match → AI fallback path.
|
|
70
|
+
const convention = findReturnConvention();
|
|
71
|
+
const m = 'Return logout confirmation'.match(convention.pattern)!;
|
|
72
|
+
const call = convention.generateCall(m, makeCtx());
|
|
73
|
+
expect(call).toBe('');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("REGRESSION: '204 No Content' returns empty (number + identifiers is a parse error)", () => {
|
|
77
|
+
const convention = findReturnConvention();
|
|
78
|
+
const m = 'Return 204 No Content'.match(convention.pattern)!;
|
|
79
|
+
const call = convention.generateCall(m, makeCtx());
|
|
80
|
+
expect(call).toBe('');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("REGRESSION: a PascalCase type name that was NEVER declared returns empty", () => {
|
|
84
|
+
// `return AuthRegisterResponse;` looks like valid TS but references
|
|
85
|
+
// a type as a value — won't resolve at runtime. Only emit when the
|
|
86
|
+
// identifier was actually declared in the body.
|
|
87
|
+
const convention = findReturnConvention();
|
|
88
|
+
const m = 'Return AuthRegisterResponse'.match(convention.pattern)!;
|
|
89
|
+
const call = convention.generateCall(m, makeCtx()); // declaredVars empty
|
|
90
|
+
expect(call).toBe('');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("when the identifier IS declared, it WILL emit (the LLM put the value in scope)", () => {
|
|
94
|
+
const convention = findReturnConvention();
|
|
95
|
+
const m = 'Return AuthRegisterResponse'.match(convention.pattern)!;
|
|
96
|
+
const ctx = makeCtx({ declaredVars: new Set(['AuthRegisterResponse']) });
|
|
97
|
+
const call = convention.generateCall(m, ctx);
|
|
98
|
+
expect(call).toContain('return AuthRegisterResponse;');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("falls back for whitespace-y multi-word values that aren't valid identifiers", () => {
|
|
102
|
+
const convention = findReturnConvention();
|
|
103
|
+
const m = 'Return the cached invoice if still fresh'.match(convention.pattern)!;
|
|
104
|
+
const call = convention.generateCall(m, makeCtx());
|
|
105
|
+
// "the cached invoice..." DOES start with "the" so model-pattern
|
|
106
|
+
// applies — emits the model var. That's the existing behaviour
|
|
107
|
+
// and intentional.
|
|
108
|
+
expect(call).toContain('return order;');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("regression: empty result string causes the matcher to fall through", async () => {
|
|
112
|
+
// Smoke-test the integration: import the shared matcher and feed
|
|
113
|
+
// it a prose 'Return X' step. Expect matched: false (AI fallback).
|
|
114
|
+
const { matchAgainstConventions } = await import('../../_shared/step-matching.js');
|
|
115
|
+
const ctx = makeCtx();
|
|
116
|
+
const result = matchAgainstConventions(
|
|
117
|
+
'Return logout confirmation',
|
|
118
|
+
ctx,
|
|
119
|
+
STEP_CONVENTIONS as any,
|
|
120
|
+
(inputs: string[]) => (inputs.length > 0 ? `{ ${inputs.join(', ')} }` : '{}'),
|
|
121
|
+
);
|
|
122
|
+
expect(result.matched).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -377,24 +377,69 @@ function generateCustomActions(controller: any, modelName: string, modelVar: str
|
|
|
377
377
|
const allUnmatchedSteps: Array<{ step: string; functionName: string; operationName: string }> = [];
|
|
378
378
|
|
|
379
379
|
Object.entries(controller.actions).forEach(([actionName, action]: [string, any]) => {
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
380
|
+
const params = action.parameters || {};
|
|
381
|
+
const paramNames = Object.keys(params);
|
|
382
|
+
const hasSteps = Array.isArray(action.steps) && action.steps.length > 0;
|
|
383
|
+
|
|
384
|
+
// Realize L3 contract: when an action declares `steps:`, the
|
|
385
|
+
// implementation lives in `behaviors/<ModelName>Controller.ai.ts`
|
|
386
|
+
// as an exported async function named EXACTLY `actionName`. Both
|
|
387
|
+
// per-action and per-owner emitters honour this contract. The
|
|
388
|
+
// controller method here just delegates: collect the action's
|
|
389
|
+
// parameters into a single `input` object and forward to the
|
|
390
|
+
// per-action implementation.
|
|
391
|
+
//
|
|
392
|
+
// Pre-this-fix, the controller emitted a per-STEP chain
|
|
393
|
+
// (`step1Result = await aiBehaviors.<verboseStepName>(...)`,
|
|
394
|
+
// `step2Result = await aiBehaviors.<verboseStepName>(step1Result)`,
|
|
395
|
+
// ...) referencing function names that NO emitter produces anymore
|
|
396
|
+
// — per-action and per-owner both export per-action names only.
|
|
397
|
+
// Every controller method was broken at compile time. Surfaced by
|
|
398
|
+
// the 2026-05-11 per-owner trial v4 (TS2339 "Property
|
|
399
|
+
// 'throwImmediatelyIfNoRefreshTokenIsHeldInLocalState' does not
|
|
400
|
+
// exist on type 'typeof import("...")'").
|
|
401
|
+
//
|
|
402
|
+
// Actions WITHOUT steps still go through the legacy
|
|
403
|
+
// behavior-generator path (deterministic shell from
|
|
404
|
+
// requires/ensures/publishes only); they don't need an .ai.ts
|
|
405
|
+
// delegate.
|
|
406
|
+
let body: string;
|
|
407
|
+
let needsAi = false;
|
|
408
|
+
if (hasSteps) {
|
|
409
|
+
// Delegate path. `input` is the bag of declared params, even if
|
|
410
|
+
// the action has zero declared params (then `input = {}`).
|
|
411
|
+
const inputObj = paramNames.length > 0
|
|
412
|
+
? `{ ${paramNames.join(', ')} }`
|
|
413
|
+
: '{}';
|
|
414
|
+
body = ` const input = ${inputObj};
|
|
415
|
+
return await aiBehaviors.${actionName}(input);`;
|
|
416
|
+
needsAi = true;
|
|
417
|
+
} else {
|
|
418
|
+
// No-steps path — use the legacy behavior-generator that
|
|
419
|
+
// emits requires/ensures/publishes scaffolding.
|
|
420
|
+
const behavior: BehaviorMetadata = {
|
|
421
|
+
preconditions: action.requires || action.preconditions || [],
|
|
422
|
+
steps: [],
|
|
423
|
+
postconditions: action.ensures || action.postconditions || [],
|
|
424
|
+
sideEffects: action.publishes || action.events || [],
|
|
425
|
+
transactional: action.transactional,
|
|
426
|
+
};
|
|
427
|
+
const ctx: BehaviorContext = {
|
|
428
|
+
modelName,
|
|
429
|
+
serviceName: `${modelName}Controller`,
|
|
430
|
+
operationName: actionName,
|
|
431
|
+
prismaModel: modelVar,
|
|
432
|
+
parameterNames: paramNames,
|
|
433
|
+
};
|
|
434
|
+
const result = generateBehaviorWithHelpers(behavior, {}, ctx);
|
|
435
|
+
allUnmatchedSteps.push(...result.unmatchedSteps);
|
|
436
|
+
body = result.body;
|
|
437
|
+
needsAi = result.unmatchedSteps.length > 0;
|
|
438
|
+
|
|
439
|
+
if (result.helperMethods.length > 0) {
|
|
440
|
+
actions.push(...result.helperMethods);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
398
443
|
|
|
399
444
|
actions.push(`
|
|
400
445
|
/**
|
|
@@ -402,12 +447,15 @@ function generateCustomActions(controller: any, modelName: string, modelVar: str
|
|
|
402
447
|
* ${action.description || ''}
|
|
403
448
|
*/
|
|
404
449
|
public async ${actionName}(${generateActionParams(action)}): Promise<any> {
|
|
405
|
-
${
|
|
450
|
+
${body}
|
|
406
451
|
}`);
|
|
407
452
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
453
|
+
// Aggregate the needsAi signal for the import-line decision below.
|
|
454
|
+
if (needsAi) allUnmatchedSteps.push({
|
|
455
|
+
step: '<delegate-to-ai-ts>',
|
|
456
|
+
functionName: actionName,
|
|
457
|
+
operationName: actionName,
|
|
458
|
+
});
|
|
411
459
|
});
|
|
412
460
|
|
|
413
461
|
return {
|
|
@@ -151,7 +151,7 @@ function generateOperations(service: any): string {
|
|
|
151
151
|
function generateOperation(operationName: string, operation: any, service: any): string {
|
|
152
152
|
const rawParams = generateOperationParams(operation);
|
|
153
153
|
const hasPublish = service.publishes && service.publishes.length > 0;
|
|
154
|
-
const body = generateOperationLogic(operation, service);
|
|
154
|
+
const body = generateOperationLogic(operation, service, operationName);
|
|
155
155
|
// Rename any operation parameter that the body doesn't reference so tsc's
|
|
156
156
|
// noUnusedParameters doesn't trip on placeholder service stubs.
|
|
157
157
|
const params = renameUnusedParams(rawParams, body);
|
|
@@ -221,7 +221,7 @@ function generateOperationParams(operation: any): string {
|
|
|
221
221
|
* Generate operation logic from behavioral metadata (L3 generation).
|
|
222
222
|
* Falls back to placeholder if no behavioral data available.
|
|
223
223
|
*/
|
|
224
|
-
function generateOperationLogic(operation: any, service: any): string {
|
|
224
|
+
function generateOperationLogic(operation: any, service: any, operationName?: string): string {
|
|
225
225
|
// Check for behavioral metadata — supports both AI-optimized format (implementation.preconditions)
|
|
226
226
|
// and SpecVerse convention format (requires/ensures/publishes)
|
|
227
227
|
const impl = operation.implementation || {};
|
|
@@ -239,14 +239,60 @@ function generateOperationLogic(operation: any, service: any): string {
|
|
|
239
239
|
.map((e: string) => `Publish event: ${e}`);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
// Realize L3 contract (mirrors controller-generator.ts): when an
|
|
243
|
+
// operation declares `steps:`, the implementation lives in
|
|
244
|
+
// `behaviors/<ServiceName>.ai.ts` as an exported async function
|
|
245
|
+
// named EXACTLY `operation.name`. Both per-action and per-owner
|
|
246
|
+
// emitters honour this contract. The service method here delegates:
|
|
247
|
+
// collect the operation's parameters into a single `input` object
|
|
248
|
+
// and forward to the per-action implementation in the .ai.ts.
|
|
249
|
+
//
|
|
250
|
+
// Pre-this-fix, the service emitted a per-STEP chain
|
|
251
|
+
// (`step1Result = await aiBehaviors.<verboseStepName>(...)`,
|
|
252
|
+
// `step2Result = await aiBehaviors.<verboseStepName>(step1Result)`,
|
|
253
|
+
// ...) referencing function names that NO emitter produces anymore
|
|
254
|
+
// — per-action and per-owner both export per-action names only.
|
|
255
|
+
// Every service method was broken at compile time. Surfaced by the
|
|
256
|
+
// 2026-05-11 per-owner trial v5: 411 TS errors in src/services/
|
|
257
|
+
// tracing back to "Property '<verboseStepName>' does not exist on
|
|
258
|
+
// type 'typeof import('.../behaviors/<X>.ai.js')'". Same defect as
|
|
259
|
+
// the controller-side per-step mismatch.
|
|
260
|
+
//
|
|
261
|
+
// Operations WITHOUT steps still go through the legacy
|
|
262
|
+
// behavior-generator path (preconditions/postconditions only,
|
|
263
|
+
// no per-step chain to break).
|
|
264
|
+
const hasSteps = Array.isArray(impl.steps) && impl.steps.length > 0
|
|
265
|
+
|| Array.isArray(operation.steps) && operation.steps.length > 0;
|
|
266
|
+
if (hasSteps) {
|
|
267
|
+
// operationName resolution: prefer the caller-supplied entry KEY
|
|
268
|
+
// (always correct when operations is a name-keyed object — the
|
|
269
|
+
// canonical spec shape), fall back to operation.name (array-shape
|
|
270
|
+
// operations), final fallback 'execute' for malformed input.
|
|
271
|
+
// Pre-this-fix line read `operation.name || 'execute'`, which
|
|
272
|
+
// landed every operation on the `execute` delegate when the spec
|
|
273
|
+
// keyed operations by name without a redundant inner `.name`
|
|
274
|
+
// (the analyse-pipeline's canonical output). Result: the service
|
|
275
|
+
// method delegated to `aiBehaviors.execute(input)` but the .ai.ts
|
|
276
|
+
// only exported the real operation names → 69 TS2339 errors per
|
|
277
|
+
// 2026-05-11 idle-meta stub trial.
|
|
278
|
+
const resolvedOpName = operationName || operation.name || 'execute';
|
|
279
|
+
const paramNames = Object.keys(operation.parameters || {});
|
|
280
|
+
const inputObj = paramNames.length > 0
|
|
281
|
+
? `{ ${paramNames.join(', ')} }`
|
|
282
|
+
: '{}';
|
|
283
|
+
return ` const input = ${inputObj};
|
|
284
|
+
return await aiBehaviors.${resolvedOpName}(input);`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (impl.preconditions?.length || impl.postconditions?.length || impl.transactional) {
|
|
288
|
+
// L3: Generate from behavioral specification (no-steps path —
|
|
289
|
+
// only requires/ensures/publishes scaffolding).
|
|
244
290
|
const modelName = inferModelFromServiceName(service.name);
|
|
245
291
|
const parameterNames = Object.keys(operation.parameters || {});
|
|
246
292
|
const context: BehaviorContext = {
|
|
247
293
|
modelName,
|
|
248
294
|
serviceName: service.name,
|
|
249
|
-
operationName: operation.name || 'execute',
|
|
295
|
+
operationName: operationName || operation.name || 'execute',
|
|
250
296
|
prismaModel: modelName,
|
|
251
297
|
parameterNames,
|
|
252
298
|
};
|
|
@@ -255,7 +301,7 @@ function generateOperationLogic(operation: any, service: any): string {
|
|
|
255
301
|
preconditions: impl.preconditions || [],
|
|
256
302
|
postconditions: impl.postconditions || [],
|
|
257
303
|
sideEffects: impl.sideEffects || [],
|
|
258
|
-
steps:
|
|
304
|
+
steps: [], // steps path handled by the delegate branch above
|
|
259
305
|
transactional: impl.transactional || false
|
|
260
306
|
};
|
|
261
307
|
|
|
@@ -330,8 +330,40 @@ export const STEP_CONVENTIONS: StepConvention[] = [
|
|
|
330
330
|
return ` // Step ${ctx.stepNum}: Return result
|
|
331
331
|
return ${toVar(ctx.modelName)};`;
|
|
332
332
|
}
|
|
333
|
-
return `
|
|
333
|
+
// Only emit a typed `return <expr>;` when `value` is a single
|
|
334
|
+
// bareword identifier (or a single dotted/bracketed accessor) —
|
|
335
|
+
// anything that's syntactically a valid TypeScript expression
|
|
336
|
+
// already declared in the action's scope. Spec text like
|
|
337
|
+
// "logout confirmation" or "204 No Content" produces a
|
|
338
|
+
// grammatical English phrase but unparseable TS:
|
|
339
|
+
// return logout confirmation; // two bare identifiers
|
|
340
|
+
// return 204 No Content; // number + identifiers
|
|
341
|
+
// Returning the empty string here makes the matcher treat this
|
|
342
|
+
// as "convention regex matched but no safe emission" — the
|
|
343
|
+
// matcher falls through to the AI-fallback ([WRITE]) shape so
|
|
344
|
+
// the per-action / per-owner LLM hook handles it. Verified
|
|
345
|
+
// empirically by the 2026-05-11 idle-meta per-owner trial:
|
|
346
|
+
// the LLM correctly stubbed actions where prose returns had
|
|
347
|
+
// been bound to broken pre-baked snippets.
|
|
348
|
+
const isSafeIdentifier = /^[A-Za-z_$][\w$]*$/.test(value);
|
|
349
|
+
const isSafePropertyAccess = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value);
|
|
350
|
+
if (isSafeIdentifier || isSafePropertyAccess) {
|
|
351
|
+
// Reference a single in-scope variable — but only if it was
|
|
352
|
+
// declared earlier in this action's emitted body (via the
|
|
353
|
+
// matcher's declaredVars set). Otherwise we'd emit
|
|
354
|
+
// `return AuthRegisterResponse;` referencing a type name
|
|
355
|
+
// that never resolves at runtime.
|
|
356
|
+
const head = value.split('.')[0]!;
|
|
357
|
+
const declared = ctx.declaredVars instanceof Set
|
|
358
|
+
? ctx.declaredVars.has(head)
|
|
359
|
+
: false;
|
|
360
|
+
if (declared) {
|
|
361
|
+
return ` // Step ${ctx.stepNum}: Return ${value}
|
|
334
362
|
return ${value};`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Bounce to [WRITE] — LLM will author this return.
|
|
366
|
+
return '';
|
|
335
367
|
},
|
|
336
368
|
},
|
|
337
369
|
];
|