@saptools/service-flow 0.1.67 → 0.1.68
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/CHANGELOG.md +10 -0
- package/README.md +20 -8
- package/TECHNICAL-NOTE.md +27 -0
- package/dist/{chunk-ZQABU7MR.js → chunk-AEM4JY22.js} +12676 -6714
- package/dist/chunk-AEM4JY22.js.map +1 -0
- package/dist/cli.js +2325 -413
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +61 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/003-doctor-package-resolution.ts +68 -0
- package/src/cli/doctor.ts +25 -14
- package/src/db/000-call-fact-repository.ts +475 -342
- package/src/db/001-fact-lifecycle.ts +60 -30
- package/src/db/002-fact-json-inventory.ts +169 -0
- package/src/db/003-current-fact-semantics.ts +698 -0
- package/src/db/004-package-target-invalidation.ts +173 -0
- package/src/db/005-schema-structure.ts +201 -0
- package/src/db/006-relative-symbol-resolution.ts +443 -0
- package/src/db/007-package-fact-semantics.ts +573 -0
- package/src/db/008-relative-fact-semantics.ts +207 -0
- package/src/db/009-binding-fact-semantics.ts +347 -0
- package/src/db/010-package-symbol-surface-semantics.ts +320 -0
- package/src/db/011-symbol-call-semantics.ts +144 -0
- package/src/db/012-binding-reference-proof.ts +264 -0
- package/src/db/migrations.ts +16 -3
- package/src/db/repositories.ts +113 -6
- package/src/db/schema.ts +4 -2
- package/src/index.ts +12 -0
- package/src/indexer/repository-indexer.ts +72 -11
- package/src/indexer/workspace-indexer.ts +123 -32
- package/src/linker/003-package-import-symbol-resolver.ts +363 -131
- package/src/linker/004-event-subscription-handler-linker.ts +2 -0
- package/src/linker/005-odata-path-structure.ts +371 -0
- package/src/linker/cross-repo-linker.ts +2 -1
- package/src/linker/odata-path-normalizer.ts +273 -180
- package/src/linker/service-resolver.ts +197 -77
- package/src/parsers/002-symbol-import-bindings.ts +516 -0
- package/src/parsers/003-package-public-surface.ts +661 -0
- package/src/parsers/004-fact-identity.ts +108 -0
- package/src/parsers/005-event-subscription-facts.ts +281 -0
- package/src/parsers/006-binding-identity.ts +343 -0
- package/src/parsers/007-source-fact-reconciliation.ts +105 -0
- package/src/parsers/008-package-surface-publication.ts +82 -0
- package/src/parsers/009-symbol-call-facts.ts +528 -0
- package/src/parsers/010-package-public-surface-analysis.ts +352 -0
- package/src/parsers/011-binding-lexical-scope.ts +583 -0
- package/src/parsers/012-package-fact-contract.ts +306 -0
- package/src/parsers/013-executable-body-eligibility.ts +35 -0
- package/src/parsers/014-service-binding-helper-flow.ts +240 -0
- package/src/parsers/015-service-binding-collector.ts +693 -0
- package/src/parsers/016-local-symbol-reference.ts +261 -0
- package/src/parsers/017-symbol-derived-contexts.ts +268 -0
- package/src/parsers/018-package-commonjs-syntax.ts +142 -0
- package/src/parsers/019-binding-assignment-targets.ts +76 -0
- package/src/parsers/020-stable-local-value.ts +217 -0
- package/src/parsers/021-binding-visibility.ts +152 -0
- package/src/parsers/operation-path-analysis.ts +6 -1
- package/src/parsers/outbound-call-parser.ts +19 -6
- package/src/parsers/package-json-parser.ts +45 -3
- package/src/parsers/service-binding-parser-helpers.ts +86 -15
- package/src/parsers/service-binding-parser.ts +147 -597
- package/src/parsers/symbol-parser.ts +482 -353
- package/src/trace/002-trace-diagnostics.ts +36 -1
- package/src/trace/016-compact-projector.ts +16 -17
- package/src/trace/020-compact-field-projection.ts +48 -37
- package/src/trace/021-compact-decision-normalization.ts +105 -0
- package/src/trace/022-trace-fact-preflight.ts +21 -0
- package/src/trace/023-nested-event-scopes.ts +23 -0
- package/src/trace/024-compact-observation-decision.ts +76 -0
- package/src/trace/025-trace-implementation-scope.ts +123 -0
- package/src/trace/026-trace-start-scope.ts +335 -0
- package/src/trace/027-trace-scope-execution.ts +566 -0
- package/src/trace/028-trace-operation-execution.ts +336 -0
- package/src/trace/029-trace-start-implementation.ts +172 -0
- package/src/trace/trace-engine.ts +122 -624
- package/src/types.ts +56 -0
- package/src/utils/001-placeholders.ts +188 -10
- package/src/version.ts +1 -1
- package/dist/chunk-ZQABU7MR.js.map +0 -1
|
@@ -18,16 +18,33 @@ import { classifyRepository } from '../discovery/classify-repository.js';
|
|
|
18
18
|
import { parseCdsFile } from '../parsers/cds-parser.js';
|
|
19
19
|
import { parseDecorators } from '../parsers/decorator-parser.js';
|
|
20
20
|
import { parseHandlerRegistrations } from '../parsers/handler-registration-parser.js';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
classifyOutboundCallsInSource,
|
|
23
|
+
parseOutboundCalls,
|
|
24
|
+
} from '../parsers/outbound-call-parser.js';
|
|
22
25
|
import { parseExecutableSymbols } from '../parsers/symbol-parser.js';
|
|
23
26
|
import {
|
|
24
27
|
loadPackageJsonSnapshot,
|
|
25
28
|
} from '../parsers/package-json-parser.js';
|
|
26
29
|
import { parseServiceBindings } from '../parsers/service-binding-parser.js';
|
|
30
|
+
import { reconcileSourceFacts } from '../parsers/007-source-fact-reconciliation.js';
|
|
31
|
+
import {
|
|
32
|
+
analyzeRepositoryPackageSurface,
|
|
33
|
+
mergePackageSymbolEvidence,
|
|
34
|
+
} from '../parsers/008-package-surface-publication.js';
|
|
35
|
+
import type {
|
|
36
|
+
PackagePublicSurfaceFact,
|
|
37
|
+
} from '../parsers/003-package-public-surface.js';
|
|
27
38
|
import { normalizePath } from '../utils/path-utils.js';
|
|
28
39
|
import { errorMessage } from '../utils/diagnostics.js';
|
|
29
40
|
import { sha256Text } from '../utils/hashing.js';
|
|
30
41
|
import { ANALYZER_VERSION } from '../version.js';
|
|
42
|
+
import {
|
|
43
|
+
createPackageInvalidationBatch,
|
|
44
|
+
finalizePackageTargetInvalidations,
|
|
45
|
+
invalidatePackageTargetFacts,
|
|
46
|
+
type PackageInvalidationBatch,
|
|
47
|
+
} from '../db/004-package-target-invalidation.js';
|
|
31
48
|
import {
|
|
32
49
|
loadRepositorySourceContext,
|
|
33
50
|
type RepositorySourceContext,
|
|
@@ -55,6 +72,7 @@ export interface PreparedRepositoryIndex extends IndexRepoResult {
|
|
|
55
72
|
fingerprint?: string;
|
|
56
73
|
kind?: string;
|
|
57
74
|
parsed?: ParsedFacts;
|
|
75
|
+
packagePublicSurface?: PackagePublicSurfaceFact;
|
|
58
76
|
}
|
|
59
77
|
export async function indexRepository(
|
|
60
78
|
db: Db,
|
|
@@ -64,7 +82,11 @@ export async function indexRepository(
|
|
|
64
82
|
): Promise<IndexRepoResult> {
|
|
65
83
|
try {
|
|
66
84
|
const prepared = await prepareRepositoryIndex(repo, force, instrumentation);
|
|
67
|
-
if (!prepared.skipped) db.transaction(() =>
|
|
85
|
+
if (!prepared.skipped) db.transaction(() => {
|
|
86
|
+
const batch = createPackageInvalidationBatch([prepared.repo.id]);
|
|
87
|
+
publishPreparedRepositoryIndex(db, prepared, batch);
|
|
88
|
+
finalizePackageTargetInvalidations(db, batch);
|
|
89
|
+
});
|
|
68
90
|
return { fileCount: prepared.fileCount, diagnosticCount: prepared.diagnosticCount, skipped: prepared.skipped };
|
|
69
91
|
} catch (error) {
|
|
70
92
|
recordIndexFailure(db, repo.id, error);
|
|
@@ -89,13 +111,21 @@ export async function prepareRepositoryIndex(
|
|
|
89
111
|
sources, packageFacts, packageSnapshot.rawText,
|
|
90
112
|
);
|
|
91
113
|
if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
92
|
-
const
|
|
114
|
+
const parsedFacts = await parseAllSourceFacts(repo.absolute_path, sources);
|
|
115
|
+
const packageSurface = analyzeRepositoryPackageSurface(
|
|
116
|
+
packageFacts, packageSnapshot.manifest, sources,
|
|
117
|
+
);
|
|
118
|
+
const parsed = {
|
|
119
|
+
...parsedFacts,
|
|
120
|
+
symbols: mergePackageSymbolEvidence(parsedFacts.symbols, packageSurface),
|
|
121
|
+
};
|
|
93
122
|
return {
|
|
94
123
|
repo,
|
|
95
124
|
packageFacts,
|
|
96
125
|
fingerprint,
|
|
97
126
|
kind: await classifyRepository(repo.absolute_path, packageFacts),
|
|
98
127
|
parsed,
|
|
128
|
+
packagePublicSurface: packageSurface.surface,
|
|
99
129
|
fileCount: sourceFiles.length,
|
|
100
130
|
diagnosticCount: parsed.handlers.filter((handler) =>
|
|
101
131
|
handler.hasHandlerDecorator
|
|
@@ -104,12 +134,31 @@ export async function prepareRepositoryIndex(
|
|
|
104
134
|
skipped: false,
|
|
105
135
|
};
|
|
106
136
|
}
|
|
107
|
-
export function publishPreparedRepositoryIndex(
|
|
137
|
+
export function publishPreparedRepositoryIndex(
|
|
138
|
+
db: Db,
|
|
139
|
+
prepared: PreparedRepositoryIndex,
|
|
140
|
+
invalidations: PackageInvalidationBatch,
|
|
141
|
+
): void {
|
|
108
142
|
if (prepared.skipped) return;
|
|
109
|
-
if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint
|
|
143
|
+
if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint
|
|
144
|
+
|| !prepared.kind || !prepared.packagePublicSurface)
|
|
145
|
+
throw new Error('Prepared repository index is missing publication facts');
|
|
110
146
|
const now = new Date().toISOString();
|
|
111
147
|
const repoId = prepared.repo.id;
|
|
112
|
-
|
|
148
|
+
invalidatePackageTargetFacts(
|
|
149
|
+
db, repoId, prepared.packageFacts.packageName, invalidations,
|
|
150
|
+
);
|
|
151
|
+
db.prepare(`UPDATE repositories SET package_name=?, package_version=?,
|
|
152
|
+
dependencies_json=?,package_public_surface_json=?,kind=?,index_status=?
|
|
153
|
+
WHERE id=?`).run(
|
|
154
|
+
prepared.packageFacts.packageName,
|
|
155
|
+
prepared.packageFacts.packageVersion,
|
|
156
|
+
JSON.stringify(prepared.packageFacts.dependencies),
|
|
157
|
+
JSON.stringify(prepared.packagePublicSurface),
|
|
158
|
+
prepared.kind,
|
|
159
|
+
'indexing',
|
|
160
|
+
repoId,
|
|
161
|
+
);
|
|
113
162
|
clearRepoFacts(db, repoId);
|
|
114
163
|
insertRequires(db, repoId, prepared.packageFacts.cdsRequires);
|
|
115
164
|
const fileStmt = db.prepare('INSERT INTO files(repo_id,relative_path,extension,sha256,size_bytes,last_indexed_at) VALUES(?,?,?,?,?,?) ON CONFLICT(repo_id,relative_path) DO UPDATE SET sha256=excluded.sha256,size_bytes=excluded.size_bytes,last_indexed_at=excluded.last_indexed_at');
|
|
@@ -139,13 +188,25 @@ async function parseAllSourceFacts(
|
|
|
139
188
|
facts.fileRecords.push({ relativePath: normalizePath(file), extension: path.extname(file), sha256: sha256Text(snapshot.text), sizeBytes: snapshot.sizeBytes });
|
|
140
189
|
if (file.endsWith('.cds')) facts.services.push(...(await parseCdsFile(root, file, sources)));
|
|
141
190
|
if (/\.[jt]s$/.test(file)) {
|
|
191
|
+
const source = snapshot.sourceFile();
|
|
192
|
+
const classified = classifyOutboundCallsInSource(source, file);
|
|
142
193
|
facts.handlers.push(...(await parseDecorators(root, file, sources)));
|
|
143
194
|
facts.registrations.push(...(await parseHandlerRegistrations(root, file, sources)));
|
|
144
|
-
|
|
145
|
-
const symbolFacts = await parseExecutableSymbols(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
195
|
+
const bindings = await parseServiceBindings(root, file, sources);
|
|
196
|
+
const symbolFacts = await parseExecutableSymbols(
|
|
197
|
+
root, file, sources, classified,
|
|
198
|
+
);
|
|
199
|
+
const outboundCalls = await parseOutboundCalls(
|
|
200
|
+
root, file, sources, classified, bindings,
|
|
201
|
+
);
|
|
202
|
+
const reconciled = reconcileSourceFacts(
|
|
203
|
+
source, classified, bindings, outboundCalls,
|
|
204
|
+
symbolFacts.symbols, symbolFacts.calls,
|
|
205
|
+
);
|
|
206
|
+
facts.bindings.push(...reconciled.bindings);
|
|
207
|
+
facts.symbols.push(...reconciled.symbols);
|
|
208
|
+
facts.symbolCalls.push(...reconciled.symbolCalls);
|
|
209
|
+
facts.calls.push(...reconciled.outboundCalls);
|
|
149
210
|
}
|
|
150
211
|
}
|
|
151
212
|
return facts;
|
|
@@ -3,6 +3,10 @@ import { listRepositories, reposByName } from '../db/repositories.js';
|
|
|
3
3
|
import { errorMessage } from '../utils/diagnostics.js';
|
|
4
4
|
import { prepareRepositoryIndex, publishPreparedRepositoryIndex, recordIndexFailure, type PreparedRepositoryIndex } from './repository-indexer.js';
|
|
5
5
|
import { materializeCdsExtensionOperations } from './cds-extension-resolver.js';
|
|
6
|
+
import {
|
|
7
|
+
createPackageInvalidationBatch,
|
|
8
|
+
finalizePackageTargetInvalidations,
|
|
9
|
+
} from '../db/004-package-target-invalidation.js';
|
|
6
10
|
// Ownerless rows predate PID coordination; this matches doctor's stale-run threshold without taking over a recent legacy writer.
|
|
7
11
|
const LEGACY_OWNER_RECOVERY_MS = 60 * 60 * 1_000;
|
|
8
12
|
type RunningIndexRow = Record<string, unknown>;
|
|
@@ -81,41 +85,128 @@ export async function indexWorkspace(
|
|
|
81
85
|
workspaceId: number,
|
|
82
86
|
options: { repo?: string; force: boolean; injectDerivedMaterializationFailure?: boolean },
|
|
83
87
|
): Promise<{ repoCount: number; indexedCount: number; skippedCount: number; fileCount: number; diagnosticCount: number }> {
|
|
84
|
-
const repos = options.repo
|
|
85
|
-
? reposByName(db, options.repo, workspaceId)
|
|
86
|
-
: listRepositories(db, workspaceId);
|
|
87
|
-
if (options.repo && repos.length === 0)
|
|
88
|
-
throw new Error(`selector_repo_not_found: no indexed repository matched ${options.repo}.`);
|
|
89
|
-
if (options.repo && repos.length > 1)
|
|
90
|
-
throw new Error(`selector_repo_ambiguous: repository selector ${options.repo} matched ${repos.length} repositories; use a unique repository name.`);
|
|
88
|
+
const repos = selectedRepositories(db, workspaceId, options.repo);
|
|
91
89
|
const runId = claimIndexRun(db, workspaceId, repos.length);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const preparedRows: PreparedRepositoryIndex[] = [];
|
|
96
|
-
let activeRepoId: number | undefined;
|
|
90
|
+
const state: PreparationState = {
|
|
91
|
+
fileCount: 0, diagnosticCount: 0, skippedCount: 0, rows: [],
|
|
92
|
+
};
|
|
97
93
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
preparedRows.push(result);
|
|
102
|
-
fileCount += result.fileCount;
|
|
103
|
-
diagnosticCount += result.diagnosticCount;
|
|
104
|
-
skippedCount += result.skipped ? 1 : 0;
|
|
105
|
-
}
|
|
106
|
-
db.transaction(() => {
|
|
107
|
-
for (const row of preparedRows) {
|
|
108
|
-
activeRepoId = row.repo.id;
|
|
109
|
-
publishPreparedRepositoryIndex(db, row);
|
|
110
|
-
}
|
|
111
|
-
if (options.injectDerivedMaterializationFailure) throw new Error('Injected derived materialization failure');
|
|
112
|
-
materializeCdsExtensionOperations(db, workspaceId);
|
|
113
|
-
db.prepare("UPDATE index_runs SET finished_at=?, status='success', file_count=?, diagnostic_count=? WHERE id=?").run(new Date().toISOString(), fileCount, diagnosticCount, runId);
|
|
114
|
-
});
|
|
115
|
-
return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
|
|
94
|
+
await prepareRepositories(repos, options.force, state);
|
|
95
|
+
publishPreparedRows(db, workspaceId, options, runId, state);
|
|
96
|
+
return indexSummary(repos.length, state);
|
|
116
97
|
} catch (error) {
|
|
117
|
-
db
|
|
118
|
-
if (activeRepoId &&
|
|
98
|
+
finishFailedRun(db, runId, state, error);
|
|
99
|
+
if (state.activeRepoId && state.rows.length < repos.length)
|
|
100
|
+
recordIndexFailure(db, state.activeRepoId, error);
|
|
119
101
|
throw error;
|
|
120
102
|
}
|
|
121
103
|
}
|
|
104
|
+
|
|
105
|
+
type IndexRepository = ReturnType<typeof listRepositories>[number];
|
|
106
|
+
interface PreparationState {
|
|
107
|
+
fileCount: number;
|
|
108
|
+
diagnosticCount: number;
|
|
109
|
+
skippedCount: number;
|
|
110
|
+
rows: PreparedRepositoryIndex[];
|
|
111
|
+
activeRepoId?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function selectedRepositories(
|
|
115
|
+
db: Db,
|
|
116
|
+
workspaceId: number,
|
|
117
|
+
repoName: string | undefined,
|
|
118
|
+
): IndexRepository[] {
|
|
119
|
+
const repos = repoName
|
|
120
|
+
? reposByName(db, repoName, workspaceId)
|
|
121
|
+
: listRepositories(db, workspaceId);
|
|
122
|
+
if (repoName && repos.length === 0)
|
|
123
|
+
throw new Error(
|
|
124
|
+
`selector_repo_not_found: no indexed repository matched ${repoName}.`,
|
|
125
|
+
);
|
|
126
|
+
if (repoName && repos.length > 1)
|
|
127
|
+
throw new Error(
|
|
128
|
+
`selector_repo_ambiguous: repository selector ${repoName} matched ${repos.length} repositories; use a unique repository name.`,
|
|
129
|
+
);
|
|
130
|
+
return repos;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function prepareRepositories(
|
|
134
|
+
repos: readonly IndexRepository[],
|
|
135
|
+
force: boolean,
|
|
136
|
+
state: PreparationState,
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
for (const repo of repos) {
|
|
139
|
+
state.activeRepoId = repo.id;
|
|
140
|
+
const result = await prepareRepositoryIndex(repo, force);
|
|
141
|
+
state.rows.push(result);
|
|
142
|
+
state.fileCount += result.fileCount;
|
|
143
|
+
state.diagnosticCount += result.diagnosticCount;
|
|
144
|
+
state.skippedCount += result.skipped ? 1 : 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function publishPreparedRows(
|
|
149
|
+
db: Db,
|
|
150
|
+
workspaceId: number,
|
|
151
|
+
options: { injectDerivedMaterializationFailure?: boolean },
|
|
152
|
+
runId: number,
|
|
153
|
+
state: PreparationState,
|
|
154
|
+
): void {
|
|
155
|
+
db.transaction(() => {
|
|
156
|
+
const invalidations = createPackageInvalidationBatch(
|
|
157
|
+
state.rows.filter((row) => !row.skipped).map((row) => row.repo.id),
|
|
158
|
+
);
|
|
159
|
+
for (const row of state.rows) {
|
|
160
|
+
state.activeRepoId = row.repo.id;
|
|
161
|
+
publishPreparedRepositoryIndex(db, row, invalidations);
|
|
162
|
+
}
|
|
163
|
+
if (options.injectDerivedMaterializationFailure)
|
|
164
|
+
throw new Error('Injected derived materialization failure');
|
|
165
|
+
materializeCdsExtensionOperations(db, workspaceId);
|
|
166
|
+
finalizePackageTargetInvalidations(db, invalidations);
|
|
167
|
+
finishSuccessfulRun(db, runId, state);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function finishSuccessfulRun(
|
|
172
|
+
db: Db,
|
|
173
|
+
runId: number,
|
|
174
|
+
state: PreparationState,
|
|
175
|
+
): void {
|
|
176
|
+
db.prepare(`UPDATE index_runs SET finished_at=?,status='success',
|
|
177
|
+
file_count=?,diagnostic_count=? WHERE id=?`).run(
|
|
178
|
+
new Date().toISOString(), state.fileCount, state.diagnosticCount, runId,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function finishFailedRun(
|
|
183
|
+
db: Db,
|
|
184
|
+
runId: number,
|
|
185
|
+
state: PreparationState,
|
|
186
|
+
error: unknown,
|
|
187
|
+
): void {
|
|
188
|
+
db.prepare(`UPDATE index_runs SET finished_at=?,status='failed',
|
|
189
|
+
file_count=?,diagnostic_count=?,error_message=? WHERE id=?`).run(
|
|
190
|
+
new Date().toISOString(), state.fileCount, state.diagnosticCount + 1,
|
|
191
|
+
errorMessage(error), runId,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function indexSummary(
|
|
196
|
+
repoCount: number,
|
|
197
|
+
state: PreparationState,
|
|
198
|
+
): {
|
|
199
|
+
repoCount: number;
|
|
200
|
+
indexedCount: number;
|
|
201
|
+
skippedCount: number;
|
|
202
|
+
fileCount: number;
|
|
203
|
+
diagnosticCount: number;
|
|
204
|
+
} {
|
|
205
|
+
return {
|
|
206
|
+
repoCount,
|
|
207
|
+
indexedCount: repoCount - state.skippedCount,
|
|
208
|
+
skippedCount: state.skippedCount,
|
|
209
|
+
fileCount: state.fileCount,
|
|
210
|
+
diagnosticCount: state.diagnosticCount,
|
|
211
|
+
};
|
|
212
|
+
}
|