sentinel-qa 0.0.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 (101) hide show
  1. package/dist/__tests__/capture-patterns.test.d.ts +2 -0
  2. package/dist/__tests__/capture-patterns.test.d.ts.map +1 -0
  3. package/dist/__tests__/capture-patterns.test.js +82 -0
  4. package/dist/__tests__/capture-patterns.test.js.map +1 -0
  5. package/dist/__tests__/event-spec-schema.test.d.ts +2 -0
  6. package/dist/__tests__/event-spec-schema.test.d.ts.map +1 -0
  7. package/dist/__tests__/event-spec-schema.test.js +75 -0
  8. package/dist/__tests__/event-spec-schema.test.js.map +1 -0
  9. package/dist/__tests__/event-validation.test.d.ts +2 -0
  10. package/dist/__tests__/event-validation.test.d.ts.map +1 -0
  11. package/dist/__tests__/event-validation.test.js +146 -0
  12. package/dist/__tests__/event-validation.test.js.map +1 -0
  13. package/dist/__tests__/markdown-report.test.d.ts +2 -0
  14. package/dist/__tests__/markdown-report.test.d.ts.map +1 -0
  15. package/dist/__tests__/markdown-report.test.js +104 -0
  16. package/dist/__tests__/markdown-report.test.js.map +1 -0
  17. package/dist/__tests__/test-status-store.test.d.ts +2 -0
  18. package/dist/__tests__/test-status-store.test.d.ts.map +1 -0
  19. package/dist/__tests__/test-status-store.test.js +197 -0
  20. package/dist/__tests__/test-status-store.test.js.map +1 -0
  21. package/dist/event-validation/capture-patterns.d.ts +26 -0
  22. package/dist/event-validation/capture-patterns.d.ts.map +1 -0
  23. package/dist/event-validation/capture-patterns.js +152 -0
  24. package/dist/event-validation/capture-patterns.js.map +1 -0
  25. package/dist/event-validation/index.d.ts +6 -0
  26. package/dist/event-validation/index.d.ts.map +1 -0
  27. package/dist/event-validation/index.js +4 -0
  28. package/dist/event-validation/index.js.map +1 -0
  29. package/dist/event-validation/schema.d.ts +59 -0
  30. package/dist/event-validation/schema.d.ts.map +1 -0
  31. package/dist/event-validation/schema.js +22 -0
  32. package/dist/event-validation/schema.js.map +1 -0
  33. package/dist/event-validation/types.d.ts +43 -0
  34. package/dist/event-validation/types.d.ts.map +1 -0
  35. package/dist/event-validation/types.js +2 -0
  36. package/dist/event-validation/types.js.map +1 -0
  37. package/dist/event-validation/validator.d.ts +14 -0
  38. package/dist/event-validation/validator.d.ts.map +1 -0
  39. package/dist/event-validation/validator.js +97 -0
  40. package/dist/event-validation/validator.js.map +1 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +43 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/registry/registry.d.ts +12 -0
  46. package/dist/registry/registry.d.ts.map +1 -0
  47. package/dist/registry/registry.js +51 -0
  48. package/dist/registry/registry.js.map +1 -0
  49. package/dist/registry/types.d.ts +28 -0
  50. package/dist/registry/types.d.ts.map +1 -0
  51. package/dist/registry/types.js +2 -0
  52. package/dist/registry/types.js.map +1 -0
  53. package/dist/report/markdown.d.ts +13 -0
  54. package/dist/report/markdown.d.ts.map +1 -0
  55. package/dist/report/markdown.js +137 -0
  56. package/dist/report/markdown.js.map +1 -0
  57. package/dist/report/report-store.d.ts +28 -0
  58. package/dist/report/report-store.d.ts.map +1 -0
  59. package/dist/report/report-store.js +69 -0
  60. package/dist/report/report-store.js.map +1 -0
  61. package/dist/schemas/tools.d.ts +40 -0
  62. package/dist/schemas/tools.d.ts.map +1 -0
  63. package/dist/schemas/tools.js +26 -0
  64. package/dist/schemas/tools.js.map +1 -0
  65. package/dist/store/test-status-store.d.ts +18 -0
  66. package/dist/store/test-status-store.d.ts.map +1 -0
  67. package/dist/store/test-status-store.js +79 -0
  68. package/dist/store/test-status-store.js.map +1 -0
  69. package/dist/store/test-store.d.ts +15 -0
  70. package/dist/store/test-store.d.ts.map +1 -0
  71. package/dist/store/test-store.js +24 -0
  72. package/dist/store/test-store.js.map +1 -0
  73. package/dist/tools/get-report.d.ts +4 -0
  74. package/dist/tools/get-report.d.ts.map +1 -0
  75. package/dist/tools/get-report.js +48 -0
  76. package/dist/tools/get-report.js.map +1 -0
  77. package/dist/tools/get-selectors.d.ts +4 -0
  78. package/dist/tools/get-selectors.d.ts.map +1 -0
  79. package/dist/tools/get-selectors.js +19 -0
  80. package/dist/tools/get-selectors.js.map +1 -0
  81. package/dist/tools/list-apps.d.ts +4 -0
  82. package/dist/tools/list-apps.d.ts.map +1 -0
  83. package/dist/tools/list-apps.js +8 -0
  84. package/dist/tools/list-apps.js.map +1 -0
  85. package/dist/tools/run-tests.d.ts +7 -0
  86. package/dist/tools/run-tests.d.ts.map +1 -0
  87. package/dist/tools/run-tests.js +233 -0
  88. package/dist/tools/run-tests.js.map +1 -0
  89. package/dist/tools/save-tests.d.ts +4 -0
  90. package/dist/tools/save-tests.d.ts.map +1 -0
  91. package/dist/tools/save-tests.js +13 -0
  92. package/dist/tools/save-tests.js.map +1 -0
  93. package/dist/utils/logger.d.ts +7 -0
  94. package/dist/utils/logger.d.ts.map +1 -0
  95. package/dist/utils/logger.js +11 -0
  96. package/dist/utils/logger.js.map +1 -0
  97. package/dist/utils/yaml-loader.d.ts +2 -0
  98. package/dist/utils/yaml-loader.d.ts.map +1 -0
  99. package/dist/utils/yaml-loader.js +7 -0
  100. package/dist/utils/yaml-loader.js.map +1 -0
  101. package/package.json +34 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Check if a value matches the expected type string.
3
+ */
4
+ function matchesType(value, expectedType) {
5
+ switch (expectedType) {
6
+ case 'string':
7
+ return typeof value === 'string';
8
+ case 'number':
9
+ return typeof value === 'number';
10
+ case 'boolean':
11
+ return typeof value === 'boolean';
12
+ default:
13
+ return true; // unknown type = allow
14
+ }
15
+ }
16
+ /**
17
+ * Validate captured events against an event spec.
18
+ *
19
+ * For each expected event in the spec:
20
+ * - Check if a matching captured event exists (by event_name)
21
+ * - If found, validate required_params (presence + type)
22
+ * - If not found, mark as missing
23
+ *
24
+ * Any captured events not in the spec are listed as unexpected.
25
+ */
26
+ export function validateEvents(spec, captured) {
27
+ const results = [];
28
+ const matchedCapturedIndices = new Set();
29
+ for (const expected of spec) {
30
+ // Find the first matching captured event by event_name
31
+ const capturedIndex = captured.findIndex((c, i) => c.event_name === expected.event_name && !matchedCapturedIndices.has(i));
32
+ if (capturedIndex === -1) {
33
+ // Missing event
34
+ results.push({
35
+ event_name: expected.event_name,
36
+ trigger: expected.trigger,
37
+ status: 'missing',
38
+ });
39
+ continue;
40
+ }
41
+ matchedCapturedIndices.add(capturedIndex);
42
+ const capturedEvent = captured[capturedIndex];
43
+ // Validate required params
44
+ const paramErrors = [];
45
+ if (expected.required_params) {
46
+ for (const [paramName, expectedType] of Object.entries(expected.required_params)) {
47
+ if (!(paramName in capturedEvent.params)) {
48
+ paramErrors.push({
49
+ param: paramName,
50
+ expected: expectedType,
51
+ got: 'missing',
52
+ });
53
+ }
54
+ else if (!matchesType(capturedEvent.params[paramName], expectedType)) {
55
+ paramErrors.push({
56
+ param: paramName,
57
+ expected: expectedType,
58
+ got: typeof capturedEvent.params[paramName],
59
+ });
60
+ }
61
+ }
62
+ }
63
+ if (paramErrors.length > 0) {
64
+ results.push({
65
+ event_name: expected.event_name,
66
+ trigger: expected.trigger,
67
+ status: 'param_error',
68
+ param_errors: paramErrors,
69
+ });
70
+ }
71
+ else {
72
+ results.push({
73
+ event_name: expected.event_name,
74
+ trigger: expected.trigger,
75
+ status: 'matched',
76
+ });
77
+ }
78
+ }
79
+ // Find unexpected events (captured but not in spec)
80
+ const specEventNames = new Set(spec.map((s) => s.event_name));
81
+ const unexpected = captured
82
+ .filter((c, i) => !matchedCapturedIndices.has(i) && !specEventNames.has(c.event_name))
83
+ .map((c) => ({ event_name: c.event_name, params: c.params }));
84
+ const matched = results.filter((r) => r.status === 'matched').length;
85
+ const missing = results.filter((r) => r.status === 'missing').length;
86
+ const paramErrorCount = results.filter((r) => r.status === 'param_error').length;
87
+ return {
88
+ total_expected: spec.length,
89
+ matched,
90
+ missing,
91
+ param_errors: paramErrorCount,
92
+ unexpected_count: unexpected.length,
93
+ results,
94
+ unexpected,
95
+ };
96
+ }
97
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/event-validation/validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { resolve, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { AppRegistry } from './registry/registry.js';
7
+ import { TestStore } from './store/test-store.js';
8
+ import { TestStatusStore } from './store/test-status-store.js';
9
+ import { ReportStore } from './report/report-store.js';
10
+ import { logger } from './utils/logger.js';
11
+ import { registerListApps } from './tools/list-apps.js';
12
+ import { registerGetSelectors } from './tools/get-selectors.js';
13
+ import { registerSaveTests } from './tools/save-tests.js';
14
+ import { registerRunTests } from './tools/run-tests.js';
15
+ import { registerGetReport } from './tools/get-report.js';
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ async function main() {
18
+ const registryDir = process.env.SENTINEL_REGISTRY_DIR ?? resolve(__dirname, '..', '..', '..', 'registry');
19
+ const registry = new AppRegistry(registryDir);
20
+ await registry.load();
21
+ const store = new TestStore();
22
+ const reportsDir = process.env.SENTINEL_REPORTS_DIR ?? resolve(__dirname, '..', '..', '..', 'reports');
23
+ const reportStore = new ReportStore(reportsDir);
24
+ const testsDir = process.env.SENTINEL_TESTS_DIR ?? resolve(__dirname, '..', '..', '..', 'tests');
25
+ const statusStore = new TestStatusStore(testsDir);
26
+ const server = new McpServer({
27
+ name: 'sentinel-qa',
28
+ version: '0.1.0',
29
+ });
30
+ registerListApps(server, registry);
31
+ registerGetSelectors(server, registry);
32
+ registerSaveTests(server, store);
33
+ registerRunTests(server, store, registry, reportStore, statusStore);
34
+ registerGetReport(server, reportStore);
35
+ const transport = new StdioServerTransport();
36
+ await server.connect(transport);
37
+ logger.info('sentinel-qa MCP server started (stdio)');
38
+ }
39
+ main().catch((err) => {
40
+ logger.error('Fatal error:', err);
41
+ process.exit(1);
42
+ });
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,KAAK,UAAU,IAAI;IACjB,MAAM,WAAW,GACf,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;IAExF,MAAM,QAAQ,GAAG,IAAI,WAAW,CAAC,WAAW,CAAC,CAAC;IAC9C,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEtB,MAAM,KAAK,GAAG,IAAI,SAAS,EAAE,CAAC;IAE9B,MAAM,UAAU,GACd,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IACtF,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC,UAAU,CAAC,CAAC;IAEhD,MAAM,QAAQ,GACZ,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAClF,MAAM,WAAW,GAAG,IAAI,eAAe,CAAC,QAAQ,CAAC,CAAC;IAElD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACnC,oBAAoB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACjC,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IACpE,iBAAiB,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAEvC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,MAAM,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;AACxD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IAClC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { AppEntry, SelectorMap, EventSpecConfig } from './types.js';
2
+ export declare class AppRegistry {
3
+ private apps;
4
+ private registryDir;
5
+ constructor(registryDir: string);
6
+ load(): Promise<void>;
7
+ listApps(): AppEntry[];
8
+ getApp(appId: string): AppEntry | undefined;
9
+ getSelectors(appId: string): Promise<SelectorMap | null>;
10
+ getEventSpec(appId: string): Promise<EventSpecConfig | null>;
11
+ }
12
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry/registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAc,QAAQ,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAErF,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAkB;IAC9B,OAAO,CAAC,WAAW,CAAS;gBAEhB,WAAW,EAAE,MAAM;IAIzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,QAAQ,IAAI,QAAQ,EAAE;IAItB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAIrC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAYxD,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;CAWnE"}
@@ -0,0 +1,51 @@
1
+ import { resolve } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { loadYaml } from '../utils/yaml-loader.js';
4
+ import { logger } from '../utils/logger.js';
5
+ export class AppRegistry {
6
+ apps = [];
7
+ registryDir;
8
+ constructor(registryDir) {
9
+ this.registryDir = registryDir;
10
+ }
11
+ async load() {
12
+ const appsPath = resolve(this.registryDir, 'apps.yaml');
13
+ if (!existsSync(appsPath)) {
14
+ logger.warn(`apps.yaml not found at ${appsPath}`);
15
+ this.apps = [];
16
+ return;
17
+ }
18
+ const config = await loadYaml(appsPath);
19
+ this.apps = config.apps ?? [];
20
+ logger.info(`Loaded ${this.apps.length} app(s) from registry`);
21
+ }
22
+ listApps() {
23
+ return this.apps;
24
+ }
25
+ getApp(appId) {
26
+ return this.apps.find((a) => a.id === appId);
27
+ }
28
+ async getSelectors(appId) {
29
+ const app = this.getApp(appId);
30
+ if (!app?.context?.selectors)
31
+ return null;
32
+ const selectorPath = resolve(this.registryDir, app.context.selectors);
33
+ if (!existsSync(selectorPath)) {
34
+ logger.warn(`Selector file not found: ${selectorPath}`);
35
+ return null;
36
+ }
37
+ return loadYaml(selectorPath);
38
+ }
39
+ async getEventSpec(appId) {
40
+ const app = this.getApp(appId);
41
+ if (!app?.context?.event_spec)
42
+ return null;
43
+ const specPath = resolve(this.registryDir, app.context.event_spec);
44
+ if (!existsSync(specPath)) {
45
+ logger.warn(`Event spec file not found: ${specPath}`);
46
+ return null;
47
+ }
48
+ return loadYaml(specPath);
49
+ }
50
+ }
51
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/registry/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAG5C,MAAM,OAAO,WAAW;IACd,IAAI,GAAe,EAAE,CAAC;IACtB,WAAW,CAAS;IAE5B,YAAY,WAAmB;QAC7B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;QACxD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,0BAA0B,QAAQ,EAAE,CAAC,CAAC;YAClD,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;YACf,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAa,QAAQ,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,MAAM,uBAAuB,CAAC,CAAC;IACjE,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,MAAM,CAAC,KAAa;QAClB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,KAAa;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS;YAAE,OAAO,IAAI,CAAC;QAE1C,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACtE,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,4BAA4B,YAAY,EAAE,CAAC,CAAC;YACxD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,QAAQ,CAAc,YAAY,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,KAAa;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU;YAAE,OAAO,IAAI,CAAC;QAE3C,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACnE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,8BAA8B,QAAQ,EAAE,CAAC,CAAC;YACtD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,QAAQ,CAAkB,QAAQ,CAAC,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,28 @@
1
+ export interface AppEntry {
2
+ id: string;
3
+ type: 'flutter' | 'web';
4
+ repo?: string;
5
+ url?: string;
6
+ prd?: string;
7
+ context?: {
8
+ selectors?: string;
9
+ event_spec?: string;
10
+ };
11
+ }
12
+ export interface AppsConfig {
13
+ apps: AppEntry[];
14
+ }
15
+ export type SelectorMap = Record<string, string>;
16
+ export interface EventParam {
17
+ [paramName: string]: string;
18
+ }
19
+ export interface EventSpecEntry {
20
+ trigger: string;
21
+ event_name: string;
22
+ required_params?: EventParam;
23
+ optional_params?: EventParam;
24
+ }
25
+ export interface EventSpecConfig {
26
+ events: EventSpecEntry[];
27
+ }
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,GAAG,KAAK,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE;QACR,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,EAAE,CAAC;CAClB;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD,MAAM,WAAW,UAAU;IACzB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,UAAU,CAAC;IAC7B,eAAe,CAAC,EAAE,UAAU,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ import type { RunResult } from '@sentinel-qa/playwright-runner';
2
+ import type { EventValidationResult } from '../event-validation/types.js';
3
+ export interface ReportMeta {
4
+ appId: string;
5
+ suite: string;
6
+ platform: string;
7
+ timestamp: string;
8
+ }
9
+ /**
10
+ * Convert a RunResult into a Markdown report string.
11
+ */
12
+ export declare function generateMarkdownReport(result: RunResult, meta: ReportMeta, eventValidation?: EventValidationResult): string;
13
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/report/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,gCAAgC,CAAC;AAC5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAgBD;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,UAAU,EAChB,eAAe,CAAC,EAAE,qBAAqB,GACtC,MAAM,CA2IR"}
@@ -0,0 +1,137 @@
1
+ function statusIcon(status) {
2
+ switch (status) {
3
+ case 'passed': return 'PASS';
4
+ case 'failed': return 'FAIL';
5
+ case 'timedOut': return 'TIMEOUT';
6
+ case 'skipped': return 'SKIP';
7
+ }
8
+ }
9
+ function formatDuration(ms) {
10
+ if (ms < 1000)
11
+ return `${ms}ms`;
12
+ return `${(ms / 1000).toFixed(2)}s`;
13
+ }
14
+ /**
15
+ * Convert a RunResult into a Markdown report string.
16
+ */
17
+ export function generateMarkdownReport(result, meta, eventValidation) {
18
+ const lines = [];
19
+ // Header
20
+ lines.push(`# Test Report: ${meta.appId}`);
21
+ lines.push('');
22
+ lines.push(`| Field | Value |`);
23
+ lines.push(`|-------|-------|`);
24
+ lines.push(`| App | ${meta.appId} |`);
25
+ lines.push(`| Suite | ${meta.suite} |`);
26
+ lines.push(`| Platform | ${meta.platform} |`);
27
+ lines.push(`| Timestamp | ${meta.timestamp} |`);
28
+ lines.push(`| Duration | ${formatDuration(result.duration)} |`);
29
+ lines.push('');
30
+ // Summary
31
+ lines.push('## Summary');
32
+ lines.push('');
33
+ lines.push(`| Total | Passed | Failed | Timed Out | Skipped |`);
34
+ lines.push(`|-------|--------|--------|-----------|---------|`);
35
+ lines.push(`| ${result.total} | ${result.passed} | ${result.failed} | ${result.timedOut} | ${result.skipped} |`);
36
+ lines.push('');
37
+ // Overall result
38
+ if (result.failed === 0 && result.timedOut === 0) {
39
+ lines.push('**Result: ALL PASSED**');
40
+ }
41
+ else {
42
+ lines.push(`**Result: ${result.failed + result.timedOut} FAILURE(S)**`);
43
+ }
44
+ lines.push('');
45
+ // Test details
46
+ lines.push('## Test Details');
47
+ lines.push('');
48
+ lines.push('| # | ID | Title | Status | Duration |');
49
+ lines.push('|---|-----|-------|--------|----------|');
50
+ result.tests.forEach((test, i) => {
51
+ lines.push(`| ${i + 1} | ${test.id} | ${test.title} | ${statusIcon(test.status)} | ${formatDuration(test.duration)} |`);
52
+ });
53
+ lines.push('');
54
+ // Failures detail
55
+ const failures = result.tests.filter((t) => t.status === 'failed' || t.status === 'timedOut');
56
+ if (failures.length > 0) {
57
+ lines.push('## Failures');
58
+ lines.push('');
59
+ for (const test of failures) {
60
+ lines.push(`### ${test.id}: ${test.title}`);
61
+ lines.push('');
62
+ lines.push(`- **Status**: ${statusIcon(test.status)}`);
63
+ lines.push(`- **Duration**: ${formatDuration(test.duration)}`);
64
+ if (test.error) {
65
+ lines.push(`- **Error**:`);
66
+ lines.push('```');
67
+ lines.push(test.error);
68
+ lines.push('```');
69
+ }
70
+ if (test.screenshotPath) {
71
+ lines.push(`- **Screenshot**: \`${test.screenshotPath}\``);
72
+ }
73
+ lines.push('');
74
+ }
75
+ }
76
+ // Event Validation (Data Log QA)
77
+ if (eventValidation) {
78
+ lines.push('## Event Validation (Data Log QA)');
79
+ lines.push('');
80
+ lines.push('| Expected | Matched | Missing | Param Errors | Unexpected |');
81
+ lines.push('|----------|---------|---------|--------------|------------|');
82
+ lines.push(`| ${eventValidation.total_expected} | ${eventValidation.matched} | ${eventValidation.missing} | ${eventValidation.param_errors} | ${eventValidation.unexpected_count} |`);
83
+ lines.push('');
84
+ if (eventValidation.missing === 0 && eventValidation.param_errors === 0 && eventValidation.unexpected_count === 0) {
85
+ lines.push('**Event Validation: ALL MATCHED**');
86
+ }
87
+ else {
88
+ lines.push('**Event Validation: ISSUES FOUND**');
89
+ }
90
+ lines.push('');
91
+ // Event details table
92
+ lines.push('### Event Results');
93
+ lines.push('');
94
+ lines.push('| Event | Trigger | Status |');
95
+ lines.push('|-------|---------|--------|');
96
+ for (const ev of eventValidation.results) {
97
+ const statusLabel = ev.status === 'matched' ? 'MATCHED'
98
+ : ev.status === 'missing' ? 'MISSING'
99
+ : 'PARAM_ERROR';
100
+ lines.push(`| ${ev.event_name} | ${ev.trigger} | ${statusLabel} |`);
101
+ }
102
+ lines.push('');
103
+ // Param errors detail
104
+ const paramErrorResults = eventValidation.results.filter((r) => r.status === 'param_error');
105
+ if (paramErrorResults.length > 0) {
106
+ lines.push('### Parameter Errors');
107
+ lines.push('');
108
+ for (const ev of paramErrorResults) {
109
+ lines.push(`**${ev.event_name}**:`);
110
+ lines.push('');
111
+ lines.push('| Parameter | Expected | Got |');
112
+ lines.push('|-----------|----------|-----|');
113
+ for (const err of ev.param_errors ?? []) {
114
+ lines.push(`| ${err.param} | ${err.expected} | ${err.got} |`);
115
+ }
116
+ lines.push('');
117
+ }
118
+ }
119
+ // Unexpected events
120
+ if (eventValidation.unexpected.length > 0) {
121
+ lines.push('### Unexpected Events');
122
+ lines.push('');
123
+ lines.push('| Event | Params |');
124
+ lines.push('|-------|--------|');
125
+ for (const ev of eventValidation.unexpected) {
126
+ lines.push(`| ${ev.event_name} | ${JSON.stringify(ev.params)} |`);
127
+ }
128
+ lines.push('');
129
+ }
130
+ }
131
+ // Footer
132
+ lines.push('---');
133
+ lines.push(`*Generated by sentinel-qa at ${meta.timestamp}*`);
134
+ lines.push('');
135
+ return lines.join('\n');
136
+ }
137
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/report/markdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { RunResult } from '@sentinel-qa/playwright-runner';
2
+ import type { ReportMeta } from './markdown.js';
3
+ import type { EventValidationResult } from '../event-validation/types.js';
4
+ /**
5
+ * Manages report storage on disk.
6
+ * Reports are saved to: <reportsDir>/<appId>/<timestamp>/report.md
7
+ */
8
+ export declare class ReportStore {
9
+ private reportsDir;
10
+ constructor(reportsDir: string);
11
+ /**
12
+ * Save a test run result as a Markdown report.
13
+ * Returns the absolute path to the saved report file.
14
+ */
15
+ save(result: RunResult, meta: Omit<ReportMeta, 'timestamp'>, eventValidation?: EventValidationResult): Promise<string>;
16
+ /**
17
+ * Get the latest report for an app.
18
+ * Returns the Markdown content and path, or null if no reports exist.
19
+ */
20
+ getLatest(appId: string): Promise<{
21
+ markdown: string;
22
+ path: string;
23
+ jsonResult: RunResult & {
24
+ meta: ReportMeta;
25
+ };
26
+ } | null>;
27
+ }
28
+ //# sourceMappingURL=report-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-store.d.ts","sourceRoot":"","sources":["../../src/report/report-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAG1E;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,UAAU,CAAS;gBAEf,UAAU,EAAE,MAAM;IAI9B;;;OAGG;IACG,IAAI,CACR,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,EACnC,eAAe,CAAC,EAAE,qBAAqB,GACtC,OAAO,CAAC,MAAM,CAAC;IAwBlB;;;OAGG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,SAAS,GAAG;YAAE,IAAI,EAAE,UAAU,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAC;CA8BjI"}
@@ -0,0 +1,69 @@
1
+ import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { generateMarkdownReport } from './markdown.js';
5
+ import { logger } from '../utils/logger.js';
6
+ /**
7
+ * Manages report storage on disk.
8
+ * Reports are saved to: <reportsDir>/<appId>/<timestamp>/report.md
9
+ */
10
+ export class ReportStore {
11
+ reportsDir;
12
+ constructor(reportsDir) {
13
+ this.reportsDir = reportsDir;
14
+ }
15
+ /**
16
+ * Save a test run result as a Markdown report.
17
+ * Returns the absolute path to the saved report file.
18
+ */
19
+ async save(result, meta, eventValidation) {
20
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
21
+ const fullMeta = { ...meta, timestamp };
22
+ const reportDir = join(this.reportsDir, meta.appId, timestamp);
23
+ await mkdir(reportDir, { recursive: true });
24
+ // Save Markdown report
25
+ const markdown = generateMarkdownReport(result, fullMeta, eventValidation);
26
+ const mdPath = join(reportDir, 'report.md');
27
+ await writeFile(mdPath, markdown, 'utf-8');
28
+ // Save raw JSON alongside for programmatic access
29
+ const jsonPath = join(reportDir, 'result.json');
30
+ const jsonData = { ...result, meta: fullMeta };
31
+ if (eventValidation) {
32
+ jsonData.event_validation = eventValidation;
33
+ }
34
+ await writeFile(jsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');
35
+ logger.info(`Report saved: ${mdPath}`);
36
+ return mdPath;
37
+ }
38
+ /**
39
+ * Get the latest report for an app.
40
+ * Returns the Markdown content and path, or null if no reports exist.
41
+ */
42
+ async getLatest(appId) {
43
+ const appDir = join(this.reportsDir, appId);
44
+ if (!existsSync(appDir))
45
+ return null;
46
+ const entries = await readdir(appDir);
47
+ if (entries.length === 0)
48
+ return null;
49
+ // Sort by name (timestamp-based, so alphabetical = chronological)
50
+ entries.sort();
51
+ const latest = entries[entries.length - 1];
52
+ const reportDir = join(appDir, latest);
53
+ const mdPath = join(reportDir, 'report.md');
54
+ const jsonPath = join(reportDir, 'result.json');
55
+ if (!existsSync(mdPath))
56
+ return null;
57
+ const markdown = await readFile(mdPath, 'utf-8');
58
+ let jsonResult;
59
+ try {
60
+ const raw = await readFile(jsonPath, 'utf-8');
61
+ jsonResult = JSON.parse(raw);
62
+ }
63
+ catch {
64
+ jsonResult = null;
65
+ }
66
+ return { markdown, path: mdPath, jsonResult };
67
+ }
68
+ }
69
+ //# sourceMappingURL=report-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-store.js","sourceRoot":"","sources":["../../src/report/report-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ export declare const getSelectorsSchema: {
3
+ app_id: z.ZodString;
4
+ };
5
+ export declare const saveTestsSchema: {
6
+ app_id: z.ZodString;
7
+ test_cases: z.ZodArray<z.ZodObject<{
8
+ id: z.ZodString;
9
+ title: z.ZodString;
10
+ confidence: z.ZodNumber;
11
+ status: z.ZodEnum<["approved", "pending"]>;
12
+ platform: z.ZodArray<z.ZodEnum<["flutter", "web"]>, "many">;
13
+ code: z.ZodString;
14
+ }, "strip", z.ZodTypeAny, {
15
+ id: string;
16
+ status: "approved" | "pending";
17
+ platform: ("flutter" | "web")[];
18
+ title: string;
19
+ code: string;
20
+ confidence: number;
21
+ }, {
22
+ id: string;
23
+ status: "approved" | "pending";
24
+ platform: ("flutter" | "web")[];
25
+ title: string;
26
+ code: string;
27
+ confidence: number;
28
+ }>, "many">;
29
+ };
30
+ export declare const runTestsSchema: {
31
+ app_id: z.ZodString;
32
+ suite: z.ZodOptional<z.ZodString>;
33
+ platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
34
+ validate_events: z.ZodOptional<z.ZodBoolean>;
35
+ include_quarantine: z.ZodOptional<z.ZodBoolean>;
36
+ };
37
+ export declare const getReportSchema: {
38
+ app_id: z.ZodString;
39
+ };
40
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/schemas/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,kBAAkB;;CAE9B,CAAC;AAEF,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;CAY3B,CAAC;AAEF,eAAO,MAAM,cAAc;;;;;;CAM1B,CAAC;AAEF,eAAO,MAAM,eAAe;;CAE3B,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ export const getSelectorsSchema = {
3
+ app_id: z.string().describe('App ID from registry'),
4
+ };
5
+ export const saveTestsSchema = {
6
+ app_id: z.string().describe('App ID'),
7
+ test_cases: z.array(z.object({
8
+ id: z.string(),
9
+ title: z.string(),
10
+ confidence: z.number(),
11
+ status: z.enum(['approved', 'pending']),
12
+ platform: z.array(z.enum(['flutter', 'web'])),
13
+ code: z.string().describe('Executable test code'),
14
+ })),
15
+ };
16
+ export const runTestsSchema = {
17
+ app_id: z.string().describe('App ID'),
18
+ suite: z.string().optional().describe('Test suite name'),
19
+ platform: z.enum(['web', 'ios', 'android']).optional().describe('Target platform'),
20
+ validate_events: z.boolean().optional().describe('Enable data log QA — validate analytics events against spec (default: false)'),
21
+ include_quarantine: z.boolean().optional().describe('Include quarantined tests in execution (default: false)'),
22
+ };
23
+ export const getReportSchema = {
24
+ app_id: z.string().describe('App ID'),
25
+ };
26
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/schemas/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;CACpD,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACrC,UAAU,EAAE,CAAC,CAAC,KAAK,CACjB,CAAC,CAAC,MAAM,CAAC;QACP,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;QACd,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QACtB,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QACvC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;QAC7C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;KAClD,CAAC,CACH;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACrC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IACxD,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IAClF,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8EAA8E,CAAC;IAChI,kBAAkB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yDAAyD,CAAC;CAC/G,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC;CACtC,CAAC"}