pi-usereq 0.5.0 → 0.6.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/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +364 -393
- package/pi-usereq/docs/REQUIREMENTS.md +20 -19
- package/pi-usereq/docs/WORKFLOW.md +21 -29
- package/src/core/agent-tool-json.ts +42 -193
- package/src/core/compress-payload.ts +7 -15
- package/src/core/find-payload.ts +21 -44
- package/src/core/reference-payload.ts +6 -14
- package/src/core/token-counter.ts +3 -121
- package/src/index.ts +64 -100
- package/tests/debug-extension-harness.test.ts +15 -21
- package/tests/extension-registration.test.ts +23 -86
package/src/core/find-payload.ts
CHANGED
|
@@ -156,10 +156,11 @@ export interface FindToolRequestSection {
|
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
158
|
* @brief Describes the summary section of the find payload.
|
|
159
|
-
* @details Exposes aggregate file, match, line, and Doxygen counts as numeric fields plus one stable search-status discriminator. The interface is compile-time only and introduces no runtime cost.
|
|
159
|
+
* @details Exposes aggregate file, match, line, and Doxygen counts as numeric fields plus one stable search-status discriminator and the normalized validation error when request parsing fails. The interface is compile-time only and introduces no runtime cost.
|
|
160
160
|
*/
|
|
161
161
|
export interface FindToolSummarySection {
|
|
162
162
|
search_status: FindSearchStatus;
|
|
163
|
+
validation_error_message?: string;
|
|
163
164
|
processable_file_count: number;
|
|
164
165
|
matched_file_count: number;
|
|
165
166
|
no_match_file_count: number;
|
|
@@ -172,22 +173,20 @@ export interface FindToolSummarySection {
|
|
|
172
173
|
|
|
173
174
|
/**
|
|
174
175
|
* @brief Describes the repository section of the find payload.
|
|
175
|
-
* @details Stores the base path, configured source-directory scope, canonical file list
|
|
176
|
+
* @details Stores the base path, configured source-directory scope, and canonical file list used during search while omitting the static supported-tag matrix because that data belongs in tool registration metadata. The interface is compile-time only and introduces no runtime cost.
|
|
176
177
|
*/
|
|
177
178
|
export interface FindToolRepositorySection {
|
|
178
179
|
root_directory_path: string;
|
|
179
180
|
source_directory_paths: string[];
|
|
180
181
|
file_count: number;
|
|
181
182
|
file_canonical_paths: string[];
|
|
182
|
-
supported_tags_by_language: Record<string, string[]>;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* @brief Describes the full agent-oriented find payload.
|
|
187
|
-
* @details
|
|
187
|
+
* @details Exposes only aggregate search totals, repository scope, and per-file match records, omitting request echoes and static supported-tag matrices that already belong in registration metadata. The interface is compile-time only and introduces no runtime cost.
|
|
188
188
|
*/
|
|
189
189
|
export interface FindToolPayload {
|
|
190
|
-
request: FindToolRequestSection;
|
|
191
190
|
summary: FindToolSummarySection;
|
|
192
191
|
repository: FindToolRepositorySection;
|
|
193
192
|
files: FindToolFileEntry[];
|
|
@@ -262,19 +261,6 @@ function buildLineRange(startLineNumber: number, endLineNumber: number): FindLin
|
|
|
262
261
|
};
|
|
263
262
|
}
|
|
264
263
|
|
|
265
|
-
/**
|
|
266
|
-
* @brief Returns the supported-tag matrix ordered for deterministic JSON emission.
|
|
267
|
-
* @details Sorts languages alphabetically and tag arrays lexicographically so downstream agents can reuse the matrix without reparsing human prose. Runtime is O(l * t log t). No side effects occur.
|
|
268
|
-
* @return {Record<string, string[]>} Supported tags keyed by canonical language identifier.
|
|
269
|
-
*/
|
|
270
|
-
function buildSupportedTagsByLanguage(): Record<string, string[]> {
|
|
271
|
-
return Object.fromEntries(
|
|
272
|
-
Object.entries(LANGUAGE_TAGS)
|
|
273
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
274
|
-
.map(([language, tagSet]) => [language, [...tagSet].sort()]),
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
264
|
/**
|
|
279
265
|
* @brief Resolves one stable symbol name from an analyzed element.
|
|
280
266
|
* @details Prefers explicit analyzer name metadata, then falls back to the derived signature or the first source line so every matched construct retains a direct-access identifier. Runtime is O(1). No side effects occur.
|
|
@@ -717,9 +703,9 @@ function analyzeFindFile(
|
|
|
717
703
|
|
|
718
704
|
/**
|
|
719
705
|
* @brief Builds the full agent-oriented find payload.
|
|
720
|
-
* @details Validates request parameters, analyzes requested files in caller order when the request is valid, preserves skipped and no-match outcomes in structured file entries, computes aggregate numeric totals, and
|
|
706
|
+
* @details Validates request parameters, analyzes requested files in caller order when the request is valid, preserves skipped and no-match outcomes in structured file entries, computes aggregate numeric totals, and omits request echoes plus the static supported-tag matrix already encoded in registration metadata. Runtime is O(F log F + S + M). Side effects are limited to filesystem reads and optional stderr logging.
|
|
721
707
|
* @param[in] options {BuildFindToolPayloadOptions} Payload-construction options.
|
|
722
|
-
* @return {FindToolPayload} Structured find payload ordered as
|
|
708
|
+
* @return {FindToolPayload} Structured find payload ordered as summary, repository, and files.
|
|
723
709
|
* @satisfies REQ-089, REQ-090, REQ-091, REQ-092, REQ-093, REQ-094, REQ-096, REQ-098
|
|
724
710
|
*/
|
|
725
711
|
export function buildFindToolPayload(options: BuildFindToolPayloadOptions): FindToolPayload {
|
|
@@ -835,28 +821,20 @@ export function buildFindToolPayload(options: BuildFindToolPayloadOptions): Find
|
|
|
835
821
|
const processableFiles = files.filter((file) => file.status !== "skipped");
|
|
836
822
|
const repositoryFileCanonicalPaths = files.map((file) => file.canonical_path);
|
|
837
823
|
|
|
824
|
+
void toolName;
|
|
825
|
+
void scope;
|
|
826
|
+
void lineNumberMode;
|
|
827
|
+
void tagFilter;
|
|
828
|
+
void pattern;
|
|
829
|
+
void canonicalRequestedPaths;
|
|
838
830
|
return {
|
|
839
|
-
request: {
|
|
840
|
-
tool_name: toolName,
|
|
841
|
-
scope,
|
|
842
|
-
base_dir_path: absoluteBaseDir,
|
|
843
|
-
line_number_mode: lineNumberMode,
|
|
844
|
-
tag_filter_text: tagFilter,
|
|
845
|
-
tag_filter_values: tagValidation.tagValues,
|
|
846
|
-
tag_filter_status: tagValidation.status,
|
|
847
|
-
tag_filter_error_message: tagValidation.errorMessage,
|
|
848
|
-
name_regex_text: pattern,
|
|
849
|
-
regex_engine: "javascript-regexp-search",
|
|
850
|
-
regex_status: regexValidation.status,
|
|
851
|
-
regex_error_message: regexValidation.errorMessage,
|
|
852
|
-
source_directory_count: sourceDirectoryPaths.length,
|
|
853
|
-
source_directory_paths: sourceDirectoryPaths.map((sourceDirectoryPath) => canonicalizeFindPath(sourceDirectoryPath, absoluteBaseDir)),
|
|
854
|
-
requested_file_count: requestedPaths.length,
|
|
855
|
-
requested_input_paths: [...requestedPaths],
|
|
856
|
-
requested_canonical_paths: canonicalRequestedPaths,
|
|
857
|
-
},
|
|
858
831
|
summary: {
|
|
859
832
|
search_status: searchStatus,
|
|
833
|
+
validation_error_message: tagValidation.status === "invalid"
|
|
834
|
+
? tagValidation.errorMessage
|
|
835
|
+
: regexValidation.status === "invalid"
|
|
836
|
+
? regexValidation.errorMessage
|
|
837
|
+
: undefined,
|
|
860
838
|
processable_file_count: processableFiles.length,
|
|
861
839
|
matched_file_count: matchedFiles.length,
|
|
862
840
|
no_match_file_count: files.filter((file) => file.status === "no_match").length,
|
|
@@ -874,7 +852,6 @@ export function buildFindToolPayload(options: BuildFindToolPayloadOptions): Find
|
|
|
874
852
|
source_directory_paths: sourceDirectoryPaths.map((sourceDirectoryPath) => canonicalizeFindPath(sourceDirectoryPath, absoluteBaseDir)),
|
|
875
853
|
file_count: repositoryFileCanonicalPaths.length,
|
|
876
854
|
file_canonical_paths: repositoryFileCanonicalPaths,
|
|
877
|
-
supported_tags_by_language: buildSupportedTagsByLanguage(),
|
|
878
855
|
},
|
|
879
856
|
files,
|
|
880
857
|
};
|
|
@@ -889,11 +866,11 @@ export function buildFindToolPayload(options: BuildFindToolPayloadOptions): Find
|
|
|
889
866
|
*/
|
|
890
867
|
export function buildFindToolExecutionStderr(payload: FindToolPayload): string {
|
|
891
868
|
const diagnostics: string[] = [];
|
|
892
|
-
if (payload.
|
|
893
|
-
diagnostics.push(`error: tag_filter: ${payload.
|
|
869
|
+
if (payload.summary.search_status === "invalid_tag_filter") {
|
|
870
|
+
diagnostics.push(`error: tag_filter: ${payload.summary.validation_error_message ?? "invalid tag filter"}`);
|
|
894
871
|
}
|
|
895
|
-
if (payload.
|
|
896
|
-
diagnostics.push(`error: name_regex: ${payload.
|
|
872
|
+
if (payload.summary.search_status === "invalid_regex") {
|
|
873
|
+
diagnostics.push(`error: name_regex: ${payload.summary.validation_error_message ?? "invalid regex"}`);
|
|
897
874
|
}
|
|
898
875
|
payload.files.forEach((file) => {
|
|
899
876
|
if (file.status === "skipped") {
|
|
@@ -182,10 +182,9 @@ export interface ReferenceToolRepositorySection {
|
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
184
|
* @brief Describes the full agent-oriented references payload.
|
|
185
|
-
* @details
|
|
185
|
+
* @details Exposes only aggregate analysis totals, repository structure, and per-file reference records, omitting request echoes that are already known to the caller or encoded in the tool registration. The interface is compile-time only and introduces no runtime cost.
|
|
186
186
|
*/
|
|
187
187
|
export interface ReferenceToolPayload {
|
|
188
|
-
request: ReferenceToolRequestSection;
|
|
189
188
|
summary: ReferenceToolSummarySection;
|
|
190
189
|
repository: ReferenceToolRepositorySection;
|
|
191
190
|
files: ReferenceToolFileEntry[];
|
|
@@ -663,9 +662,9 @@ function analyzeReferenceFile(
|
|
|
663
662
|
|
|
664
663
|
/**
|
|
665
664
|
* @brief Builds the full agent-oriented references payload.
|
|
666
|
-
* @details Validates requested paths against the filesystem, analyzes processable files in caller order, preserves skipped and failed inputs in structured file entries, computes aggregate numeric totals, and emits
|
|
665
|
+
* @details Validates requested paths against the filesystem, analyzes processable files in caller order, preserves skipped and failed inputs in structured file entries, computes aggregate numeric totals, and emits structured repository data without echoing request metadata already known to the caller. Runtime is O(F log F + S). Side effects are limited to filesystem reads and optional stderr logging.
|
|
667
666
|
* @param[in] options {BuildReferenceToolPayloadOptions} Payload-construction options.
|
|
668
|
-
* @return {ReferenceToolPayload} Structured references payload ordered as
|
|
667
|
+
* @return {ReferenceToolPayload} Structured references payload ordered as summary, repository, and files.
|
|
669
668
|
* @satisfies REQ-011, REQ-014, REQ-076, REQ-077, REQ-078, REQ-079
|
|
670
669
|
*/
|
|
671
670
|
export function buildReferenceToolPayload(options: BuildReferenceToolPayloadOptions): ReferenceToolPayload {
|
|
@@ -761,17 +760,10 @@ export function buildReferenceToolPayload(options: BuildReferenceToolPayloadOpti
|
|
|
761
760
|
const repositoryFileCanonicalPaths = [...new Set(analyzedFiles.map((file) => file.canonical_path))]
|
|
762
761
|
.sort((left, right) => left.localeCompare(right));
|
|
763
762
|
|
|
763
|
+
void toolName;
|
|
764
|
+
void scope;
|
|
765
|
+
void canonicalRequestedPaths;
|
|
764
766
|
return {
|
|
765
|
-
request: {
|
|
766
|
-
tool_name: toolName,
|
|
767
|
-
scope,
|
|
768
|
-
base_dir_path: absoluteBaseDir,
|
|
769
|
-
source_directory_count: sourceDirectoryPaths.length,
|
|
770
|
-
source_directory_paths: sourceDirectoryPaths.map((sourceDirectoryPath) => canonicalizeReferencePath(sourceDirectoryPath, absoluteBaseDir)),
|
|
771
|
-
requested_file_count: requestedPaths.length,
|
|
772
|
-
requested_input_paths: [...requestedPaths],
|
|
773
|
-
requested_canonical_paths: canonicalRequestedPaths,
|
|
774
|
-
},
|
|
775
767
|
summary: {
|
|
776
768
|
processable_file_count: processableFiles.length,
|
|
777
769
|
analyzed_file_count: analyzedFiles.length,
|
|
@@ -175,13 +175,11 @@ export interface TokenToolGuidanceSection {
|
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
177
|
* @brief Describes the full agent-oriented token payload.
|
|
178
|
-
* @details
|
|
178
|
+
* @details Exposes only aggregate numeric totals plus per-file metrics, omitting request echoes and derived guidance that can be inferred from tool registration or recomputed by the caller. The interface is compile-time only and introduces no runtime cost.
|
|
179
179
|
*/
|
|
180
180
|
export interface TokenToolPayload {
|
|
181
|
-
request: TokenToolRequestSection;
|
|
182
181
|
summary: TokenToolSummarySection;
|
|
183
182
|
files: TokenToolFileEntry[];
|
|
184
|
-
guidance: TokenToolGuidanceSection;
|
|
185
183
|
}
|
|
186
184
|
|
|
187
185
|
/**
|
|
@@ -359,41 +357,6 @@ function roundRatio(numerator: number, denominator: number): number {
|
|
|
359
357
|
return Number((numerator / denominator).toFixed(6));
|
|
360
358
|
}
|
|
361
359
|
|
|
362
|
-
/**
|
|
363
|
-
* @brief Orders canonical file paths by one numeric metric while removing duplicates.
|
|
364
|
-
* @details Filters to counted file entries, sorts by the supplied metric direction, breaks ties by canonical path, and preserves only the first occurrence of each path. Runtime is O(n log n). No external state is mutated.
|
|
365
|
-
* @param[in] files {TokenToolFileEntry[]} Token payload file entries.
|
|
366
|
-
* @param[in] metric {(entry: TokenToolFileEntry) => number} Numeric metric selector.
|
|
367
|
-
* @param[in] direction {"asc" | "desc"} Sort direction.
|
|
368
|
-
* @return {string[]} Unique canonical paths ordered by the requested metric.
|
|
369
|
-
*/
|
|
370
|
-
function orderPathsByMetric(
|
|
371
|
-
files: TokenToolFileEntry[],
|
|
372
|
-
metric: (entry: TokenToolFileEntry) => number,
|
|
373
|
-
direction: "asc" | "desc",
|
|
374
|
-
): string[] {
|
|
375
|
-
const sorted = files
|
|
376
|
-
.filter((entry) => entry.status === "counted")
|
|
377
|
-
.sort((left, right) => {
|
|
378
|
-
const leftMetric = metric(left);
|
|
379
|
-
const rightMetric = metric(right);
|
|
380
|
-
if (leftMetric === rightMetric) {
|
|
381
|
-
return left.canonical_path.localeCompare(right.canonical_path);
|
|
382
|
-
}
|
|
383
|
-
return direction === "desc" ? rightMetric - leftMetric : leftMetric - rightMetric;
|
|
384
|
-
});
|
|
385
|
-
const seen = new Set<string>();
|
|
386
|
-
const orderedPaths: string[] = [];
|
|
387
|
-
for (const entry of sorted) {
|
|
388
|
-
if (seen.has(entry.canonical_path)) {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
seen.add(entry.canonical_path);
|
|
392
|
-
orderedPaths.push(entry.canonical_path);
|
|
393
|
-
}
|
|
394
|
-
return orderedPaths;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
360
|
/**
|
|
398
361
|
* @brief Probes one requested path before token counting.
|
|
399
362
|
* @details Resolves whether the target exists and is a regular file while capturing a stable skip reason for missing or non-file inputs. Runtime is dominated by one filesystem stat. Side effects are limited to filesystem reads.
|
|
@@ -496,9 +459,9 @@ export function countFilesMetrics(filePaths: string[], encodingName = TOKEN_COUN
|
|
|
496
459
|
|
|
497
460
|
/**
|
|
498
461
|
* @brief Builds the agent-oriented JSON payload for token-centric tools.
|
|
499
|
-
* @details Validates requested paths against the filesystem, counts token metrics for processable files, preserves caller order in the file table,
|
|
462
|
+
* @details Validates requested paths against the filesystem, counts token metrics for processable files, preserves caller order in the file table, and emits direct-access file facts such as sizes, headings, and optional Doxygen file fields while omitting request echoes and derived guidance. Runtime is O(F + S). Side effects are limited to filesystem reads.
|
|
500
463
|
* @param[in] options {BuildTokenToolPayloadOptions} Payload-construction options.
|
|
501
|
-
* @return {TokenToolPayload} Structured token payload ordered as
|
|
464
|
+
* @return {TokenToolPayload} Structured token payload ordered as summary then files.
|
|
502
465
|
* @satisfies REQ-010, REQ-017, REQ-069, REQ-070, REQ-071, REQ-073, REQ-074, REQ-075
|
|
503
466
|
*/
|
|
504
467
|
export function buildTokenToolPayload(options: BuildTokenToolPayloadOptions): TokenToolPayload {
|
|
@@ -596,71 +559,7 @@ export function buildTokenToolPayload(options: BuildTokenToolPayloadOptions): To
|
|
|
596
559
|
const countedFileCount = filesWithShares.filter((entry) => entry.status === "counted").length;
|
|
597
560
|
const errorFileCount = filesWithShares.filter((entry) => entry.status === "error").length;
|
|
598
561
|
const skippedFileCount = filesWithShares.filter((entry) => entry.status === "skipped").length;
|
|
599
|
-
const countedPathsByTokenCountDesc = orderPathsByMetric(filesWithShares, (entry) => entry.token_count, "desc");
|
|
600
|
-
const countedPathsByTokenCountAsc = orderPathsByMetric(filesWithShares, (entry) => entry.token_count, "asc");
|
|
601
|
-
const countedPathsByLineCountDesc = orderPathsByMetric(filesWithShares, (entry) => entry.line_count, "desc");
|
|
602
|
-
const dominantTokenFileEntry = filesWithShares
|
|
603
|
-
.filter((entry) => entry.status === "counted")
|
|
604
|
-
.sort((left, right) => {
|
|
605
|
-
if (left.token_count === right.token_count) {
|
|
606
|
-
return left.canonical_path.localeCompare(right.canonical_path);
|
|
607
|
-
}
|
|
608
|
-
return right.token_count - left.token_count;
|
|
609
|
-
})[0];
|
|
610
|
-
const skippedInputs = filesWithShares
|
|
611
|
-
.filter((entry) => entry.status === "skipped" && entry.error_message)
|
|
612
|
-
.map((entry) => ({
|
|
613
|
-
input_path: entry.input_path,
|
|
614
|
-
canonical_path: entry.canonical_path,
|
|
615
|
-
reason: entry.error_message!,
|
|
616
|
-
}));
|
|
617
|
-
const errorInputs = filesWithShares
|
|
618
|
-
.filter((entry) => entry.status === "error" && entry.error_message)
|
|
619
|
-
.map((entry) => ({
|
|
620
|
-
input_path: entry.input_path,
|
|
621
|
-
canonical_path: entry.canonical_path,
|
|
622
|
-
reason: entry.error_message!,
|
|
623
|
-
}));
|
|
624
|
-
const derivedRecommendations: TokenToolRecommendation[] = countedPathsByTokenCountDesc.length > 0
|
|
625
|
-
? [
|
|
626
|
-
{
|
|
627
|
-
kind: "prioritize_high_token_paths",
|
|
628
|
-
basis_metric_name: "token_count",
|
|
629
|
-
ordered_paths: countedPathsByTokenCountDesc,
|
|
630
|
-
},
|
|
631
|
-
{
|
|
632
|
-
kind: "defer_low_token_paths",
|
|
633
|
-
basis_metric_name: "token_count",
|
|
634
|
-
ordered_paths: countedPathsByTokenCountAsc,
|
|
635
|
-
},
|
|
636
|
-
]
|
|
637
|
-
: [];
|
|
638
|
-
const actionableNextSteps: TokenToolNextStepHint[] = countedPathsByTokenCountDesc.length > 0
|
|
639
|
-
? [
|
|
640
|
-
{
|
|
641
|
-
kind: "read_top_token_paths_first",
|
|
642
|
-
ordered_paths: countedPathsByTokenCountDesc.slice(0, 3),
|
|
643
|
-
goal: "minimize context-truncation risk during initial review",
|
|
644
|
-
},
|
|
645
|
-
{
|
|
646
|
-
kind: "reserve_low_token_paths_for_follow_up",
|
|
647
|
-
ordered_paths: countedPathsByTokenCountAsc.slice(0, 3),
|
|
648
|
-
goal: "defer lower-cost files until high-cost files have been reviewed",
|
|
649
|
-
},
|
|
650
|
-
]
|
|
651
|
-
: [];
|
|
652
562
|
return {
|
|
653
|
-
request: {
|
|
654
|
-
tool_name: options.toolName,
|
|
655
|
-
scope: options.scope,
|
|
656
|
-
encoding_name: encodingName,
|
|
657
|
-
base_dir_path: baseDir.split(path.sep).join("/"),
|
|
658
|
-
requested_file_count: options.requestedPaths.length,
|
|
659
|
-
requested_input_paths: options.requestedPaths,
|
|
660
|
-
requested_canonical_paths: requestedEntries.map((entry) => entry.canonicalPath),
|
|
661
|
-
docs_dir_path: options.docsDir,
|
|
662
|
-
canonical_doc_names: options.canonicalDocNames,
|
|
663
|
-
},
|
|
664
563
|
summary: {
|
|
665
564
|
processable_file_count: countedFileCount + errorFileCount,
|
|
666
565
|
counted_file_count: countedFileCount,
|
|
@@ -676,23 +575,6 @@ export function buildTokenToolPayload(options: BuildTokenToolPayloadOptions): To
|
|
|
676
575
|
average_line_count_per_counted_file: countedFileCount === 0 ? 0 : Number((totalLineCount / countedFileCount).toFixed(6)),
|
|
677
576
|
},
|
|
678
577
|
files: filesWithShares,
|
|
679
|
-
guidance: {
|
|
680
|
-
source_observations: {
|
|
681
|
-
counted_paths_by_token_count_desc: countedPathsByTokenCountDesc,
|
|
682
|
-
counted_paths_by_line_count_desc: countedPathsByLineCountDesc,
|
|
683
|
-
skipped_inputs: skippedInputs,
|
|
684
|
-
error_inputs: errorInputs,
|
|
685
|
-
dominant_token_file: dominantTokenFileEntry
|
|
686
|
-
? {
|
|
687
|
-
canonical_path: dominantTokenFileEntry.canonical_path,
|
|
688
|
-
token_count: dominantTokenFileEntry.token_count,
|
|
689
|
-
token_share: dominantTokenFileEntry.token_share,
|
|
690
|
-
}
|
|
691
|
-
: undefined,
|
|
692
|
-
},
|
|
693
|
-
derived_recommendations: derivedRecommendations,
|
|
694
|
-
actionable_next_steps: actionableNextSteps,
|
|
695
|
-
},
|
|
696
578
|
};
|
|
697
579
|
}
|
|
698
580
|
|