trxa-tool 0.0.0-stage → 0.2.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.
Files changed (155) hide show
  1. package/.claude/skills/replay-trxa-testplan/SKILL.md +627 -0
  2. package/LICENSE +120 -0
  3. package/MANUAL.md +556 -0
  4. package/README.md +91 -3
  5. package/SETUP.md +401 -0
  6. package/STUDIO.md +427 -0
  7. package/dist/analyze.d.ts +58 -0
  8. package/dist/analyze.js +122 -0
  9. package/dist/bpm.d.ts +40 -0
  10. package/dist/bpm.js +60 -0
  11. package/dist/build-test-script.d.ts +24 -0
  12. package/dist/build-test-script.js +40 -0
  13. package/dist/csv.d.ts +1 -0
  14. package/dist/csv.js +11 -0
  15. package/dist/extract.d.ts +14 -0
  16. package/dist/extract.js +34 -0
  17. package/dist/fixture-schema.d.ts +52 -0
  18. package/dist/fixture-schema.js +112 -0
  19. package/dist/fixture-store.d.ts +37 -0
  20. package/dist/fixture-store.js +75 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +850 -0
  23. package/dist/library-archive.d.ts +37 -0
  24. package/dist/library-archive.js +106 -0
  25. package/dist/mapping.d.ts +25 -0
  26. package/dist/mapping.js +393 -0
  27. package/dist/mcp-bridge.d.ts +52 -0
  28. package/dist/mcp-bridge.js +290 -0
  29. package/dist/normalize.d.ts +21 -0
  30. package/dist/normalize.js +43 -0
  31. package/dist/parse.d.ts +3 -0
  32. package/dist/parse.js +233 -0
  33. package/dist/plan-runner.d.ts +27 -0
  34. package/dist/plan-runner.js +36 -0
  35. package/dist/plan-schema.d.ts +27 -0
  36. package/dist/plan-schema.js +45 -0
  37. package/dist/planrun-schema.d.ts +53 -0
  38. package/dist/planrun-schema.js +111 -0
  39. package/dist/planrun.d.ts +20 -0
  40. package/dist/planrun.js +46 -0
  41. package/dist/report-schema.d.ts +92 -0
  42. package/dist/report-schema.js +115 -0
  43. package/dist/report.d.ts +45 -0
  44. package/dist/report.js +77 -0
  45. package/dist/schema.d.ts +96 -0
  46. package/dist/schema.js +10 -0
  47. package/dist/scrub.d.ts +44 -0
  48. package/dist/scrub.js +166 -0
  49. package/dist/suggest-inputs.d.ts +19 -0
  50. package/dist/suggest-inputs.js +92 -0
  51. package/dist/suite-runner.d.ts +59 -0
  52. package/dist/suite-runner.js +204 -0
  53. package/dist/suite-schema.d.ts +59 -0
  54. package/dist/suite-schema.js +95 -0
  55. package/dist/testcase-schema.d.ts +102 -0
  56. package/dist/testcase-schema.js +132 -0
  57. package/dist/token-usage.d.ts +38 -0
  58. package/dist/token-usage.js +95 -0
  59. package/media/studio/fixtures-test-cases.png +0 -0
  60. package/media/studio/plans-editor.png +0 -0
  61. package/media/studio/recordings-create-test-case.png +0 -0
  62. package/media/studio/run-dialog.png +0 -0
  63. package/media/studio/settings-agent-runs.png +0 -0
  64. package/media/studio/settings-working-folder.png +0 -0
  65. package/media/studio/studio-recordings.png +0 -0
  66. package/media/studio/suites-canvas.png +0 -0
  67. package/media/studio/suites-inspector.png +0 -0
  68. package/media/studio/testcases-expectations.png +0 -0
  69. package/media/studio/testcases-outputs.png +0 -0
  70. package/media/studio/testrun-details.png +0 -0
  71. package/media/studio/testruns-list.png +0 -0
  72. package/package.json +62 -6
  73. package/studio/client-dist/assets/CreatePanel-C5bb5OIs.js +6 -0
  74. package/studio/client-dist/assets/FactTile-G1pURLys.js +1 -0
  75. package/studio/client-dist/assets/FieldLabel-DPmIEQ08.js +1 -0
  76. package/studio/client-dist/assets/FixtureNamePicker-137AAgwZ.js +1 -0
  77. package/studio/client-dist/assets/Fixtures-D0t-soph.js +1 -0
  78. package/studio/client-dist/assets/GeneralSection-CWkTeSX2.js +1 -0
  79. package/studio/client-dist/assets/ListPaneHeader-DXKqX36N.js +1 -0
  80. package/studio/client-dist/assets/Plans-C70evkGc.js +1 -0
  81. package/studio/client-dist/assets/Recordings-BT0dyVLs.js +1 -0
  82. package/studio/client-dist/assets/Settings-k-J3u5lH.js +1 -0
  83. package/studio/client-dist/assets/Suites-DLioOiRN.css +1 -0
  84. package/studio/client-dist/assets/Suites-DQhhMxjT.js +7 -0
  85. package/studio/client-dist/assets/TestCases-BdiopVGI.js +1 -0
  86. package/studio/client-dist/assets/TestRuns-CylAe-3t.js +9 -0
  87. package/studio/client-dist/assets/api-DJZ703_F.js +24 -0
  88. package/studio/client-dist/assets/bucket-18-viuwGcaj.js +1 -0
  89. package/studio/client-dist/assets/fixtureName-CTZT_jKu.js +1 -0
  90. package/studio/client-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  91. package/studio/client-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  92. package/studio/client-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  93. package/studio/client-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  94. package/studio/client-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  95. package/studio/client-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  96. package/studio/client-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  97. package/studio/client-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  98. package/studio/client-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  99. package/studio/client-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  100. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-300-normal-LoaTnByX.woff +0 -0
  101. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-300-normal-RhT0Ayi3.woff2 +0 -0
  102. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-italic-Bf1n82Tb.woff +0 -0
  103. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-italic-D9J6q4_O.woff2 +0 -0
  104. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  105. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  106. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  107. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  108. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-300-normal-Bbd5v1xK.woff +0 -0
  109. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-300-normal-DqmgDkIN.woff2 +0 -0
  110. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-italic-BELOEI8R.woff2 +0 -0
  111. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-italic-CfSXsIbO.woff +0 -0
  112. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  113. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  114. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  115. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  116. package/studio/client-dist/assets/ibm-plex-sans-greek-300-normal-J-CBqZoJ.woff2 +0 -0
  117. package/studio/client-dist/assets/ibm-plex-sans-greek-300-normal-STc8XiDr.woff +0 -0
  118. package/studio/client-dist/assets/ibm-plex-sans-greek-400-italic-BEZwmy7D.woff2 +0 -0
  119. package/studio/client-dist/assets/ibm-plex-sans-greek-400-italic-CTYLy0lX.woff +0 -0
  120. package/studio/client-dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  121. package/studio/client-dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  122. package/studio/client-dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  123. package/studio/client-dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  124. package/studio/client-dist/assets/ibm-plex-sans-latin-300-normal-C9Of02-5.woff +0 -0
  125. package/studio/client-dist/assets/ibm-plex-sans-latin-300-normal-b4i7YzLG.woff2 +0 -0
  126. package/studio/client-dist/assets/ibm-plex-sans-latin-400-italic-CZTNEAuW.woff2 +0 -0
  127. package/studio/client-dist/assets/ibm-plex-sans-latin-400-italic-CsGl1sm0.woff +0 -0
  128. package/studio/client-dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  129. package/studio/client-dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  130. package/studio/client-dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  131. package/studio/client-dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  132. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-300-normal-B8Zgwjh3.woff +0 -0
  133. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-300-normal-BLFuR0P1.woff2 +0 -0
  134. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-italic-Du9zzoL2.woff2 +0 -0
  135. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-italic-V_9i1P_m.woff +0 -0
  136. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  137. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  138. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  139. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  140. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-300-normal-CkYRZD2a.woff +0 -0
  141. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-300-normal-SDfX6ktj.woff2 +0 -0
  142. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-italic-78jqtfZH.woff +0 -0
  143. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-italic-BbRnwouv.woff2 +0 -0
  144. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  145. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  146. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  147. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  148. package/studio/client-dist/assets/index-cxBc2iRv.js +10 -0
  149. package/studio/client-dist/assets/index-wjWuVHXN.css +1 -0
  150. package/studio/client-dist/assets/status-DUGxC7G7.js +1 -0
  151. package/studio/client-dist/assets/testCaseTree-Cl1-bc5O.js +1 -0
  152. package/studio/client-dist/axtr-icon.svg +2 -0
  153. package/studio/client-dist/favicon.svg +2 -0
  154. package/studio/client-dist/index.html +15 -0
  155. package/studio/server/dist/index.js +3176 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Types mirroring the WCF DataContract shape of a D365 F&O Task Recorder
3
+ * `Recording.xml` (namespace `.../Microsoft.Dynamics.Client.ServerForm.TaskRecording`).
4
+ *
5
+ * Verified against a real sample ("Post Packing Slip.axtr"): Scope, MenuItemUserAction,
6
+ * CommandUserAction, PropertyUserAction. ValidationUserAction/InfoUserAction/TaskUserAction
7
+ * are documented in the D365FO metadata (SysTaskRecorderSerializer.handle* methods) but did
8
+ * not appear in the sample, so they fall back to UnknownUserAction and are never rejected.
9
+ */
10
+ export interface FormContext {
11
+ FormID: string;
12
+ FormName: string;
13
+ RecordingName: string;
14
+ HasMetadata: boolean;
15
+ Sequence: number;
16
+ Annotations: unknown;
17
+ }
18
+ interface NodeBase {
19
+ Id: string;
20
+ Description: string;
21
+ CustomDescription: string | null;
22
+ ParentSequence: number;
23
+ Sequence: number;
24
+ ScreenshotUri: string | null;
25
+ }
26
+ export interface ScopeNode extends NodeBase {
27
+ kind: "Scope";
28
+ Name: string;
29
+ IsForm: boolean;
30
+ IsStepGroup: boolean;
31
+ ScopeType: string;
32
+ Children: RecordingNode[];
33
+ }
34
+ interface UserActionBase extends NodeBase {
35
+ GlobalId: string;
36
+ SystemGenerated: boolean;
37
+ }
38
+ export interface MenuItemUserAction extends UserActionBase {
39
+ kind: "MenuItemUserAction";
40
+ MenuItemName: string;
41
+ MenuItemType: string;
42
+ NavigationPath: string[];
43
+ }
44
+ export interface CommandArgument {
45
+ IsReference: boolean;
46
+ Value: string;
47
+ }
48
+ export interface CommandUserAction extends UserActionBase {
49
+ kind: "CommandUserAction";
50
+ FormId: string;
51
+ ControlName: string;
52
+ ControlType: string;
53
+ ControlLabel: string;
54
+ ControlLabelId: string;
55
+ CommandName: string;
56
+ Arguments: CommandArgument[];
57
+ ListContext: string | null;
58
+ RowIndex: number | null;
59
+ ReturnType: string;
60
+ ReturnValue: string | null;
61
+ TaskId: string | null;
62
+ }
63
+ export interface PropertyUserAction extends UserActionBase {
64
+ kind: "PropertyUserAction";
65
+ FormId: string;
66
+ ControlName: string;
67
+ ControlType: string;
68
+ ControlLabel: string;
69
+ ControlLabelId: string;
70
+ BoundDataSource: string;
71
+ BoundProperty: string;
72
+ PropertyName: string;
73
+ /** e.g. "Input" for a value entered by the user; other values are unverified. */
74
+ UserActionType: string;
75
+ Value: string;
76
+ ValueLabel: string | null;
77
+ IsReference: boolean;
78
+ ListContext: string | null;
79
+ RowIndex: number | null;
80
+ }
81
+ /** Fallback for node types not verified against a real sample (Validation/Info/Task user actions). */
82
+ export interface UnknownUserAction extends UserActionBase {
83
+ kind: "Unknown";
84
+ xsiType: string;
85
+ raw: Record<string, unknown>;
86
+ }
87
+ export type RecordingNode = ScopeNode | MenuItemUserAction | CommandUserAction | PropertyUserAction | UnknownUserAction;
88
+ export interface Recording {
89
+ CanonicalId: string;
90
+ CurrentSequence: number;
91
+ Description: string;
92
+ Name: string;
93
+ FormContexts: Record<string, FormContext>;
94
+ RootScope: ScopeNode;
95
+ }
96
+ export {};
package/dist/schema.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Types mirroring the WCF DataContract shape of a D365 F&O Task Recorder
3
+ * `Recording.xml` (namespace `.../Microsoft.Dynamics.Client.ServerForm.TaskRecording`).
4
+ *
5
+ * Verified against a real sample ("Post Packing Slip.axtr"): Scope, MenuItemUserAction,
6
+ * CommandUserAction, PropertyUserAction. ValidationUserAction/InfoUserAction/TaskUserAction
7
+ * are documented in the D365FO metadata (SysTaskRecorderSerializer.handle* methods) but did
8
+ * not appear in the sample, so they fall back to UnknownUserAction and are never rejected.
9
+ */
10
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Strips literal recorded data (account numbers, amounts, names, dates -- anything a user typed
3
+ * or that D365FO substituted into an auto-generated step description) out of a parsed Recording
4
+ * tree, replacing each with an opaque token. Every stripped value is also returned as a row so a
5
+ * human can review it locally and, if desired, remap it to an equivalent value from a sandbox
6
+ * environment for full-cycle replay testing.
7
+ *
8
+ * This is deliberately over-cautious: full-field redaction (not substring matching inside
9
+ * descriptions) and blanket redaction of `Unknown.raw`, since those node types are unverified and
10
+ * we don't know what they carry. Anything kept un-redacted is structural metadata (control names,
11
+ * command names, form names, menu paths) that D365FO itself defines in the AOT, not
12
+ * customer/business data.
13
+ *
14
+ * One deliberate exception to "full-field redaction": the FilterManager/QuickFilter/FilterPane
15
+ * command family (see `mapping.ts`'s `filterManagerHint`) embeds a JSON blob in one `Arguments[i]`
16
+ * entry that mixes a literal filter value with pure AOT structural metadata
17
+ * (`DataSourceName`/`FieldName`/`Operator`). Blanket-redacting that whole string used to make
18
+ * `analyze-library`'s own hint computation unable to parse it, so this family's steps showed up as
19
+ * low/unmapped in confidence counts even though `mapping.ts` resolves them correctly against real
20
+ * (unscrubbed) recordings. `scrubFilterFamilyArgument` below parses that one argument's JSON and
21
+ * redacts only the value-bearing fields, leaving the structural ones visible -- everything else in
22
+ * the recording keeps the blanket-redaction default above.
23
+ */
24
+ import type { Recording } from "./schema.js";
25
+ export interface ScrubbedValue {
26
+ recordingFile: string;
27
+ recordingName: string;
28
+ nodeKind: string;
29
+ nodeId: string;
30
+ globalId: string | null;
31
+ sequence: number;
32
+ fieldPath: string;
33
+ token: string;
34
+ originalValue: string;
35
+ }
36
+ export interface ScrubResult {
37
+ recording: Recording;
38
+ values: ScrubbedValue[];
39
+ }
40
+ /**
41
+ * Returns a deep copy of `recording` with every literal recorded value replaced by an opaque
42
+ * token, plus the list of stripped values (original text included) for local-only review.
43
+ */
44
+ export declare function scrubRecording(recording: Recording, recordingFile: string): ScrubResult;
package/dist/scrub.js ADDED
@@ -0,0 +1,166 @@
1
+ /** Command/control combos from `mapping.ts`'s `filterManagerHint`, and the one `Arguments` index
2
+ * that carries the DataSourceName/FieldName/Operator/Values JSON for each -- kept in sync with
3
+ * that function manually; update both if this family's recorded shape ever changes.
4
+ * `GetFilters:FilterManager` isn't listed: every occurrence confirmed live has an empty
5
+ * `Arguments` array (see `filterManagerHint`'s comment), so there's nothing to selectively
6
+ * redact -- it falls through to the blanket per-argument redaction below like any other command. */
7
+ const FILTER_FAMILY_JSON_ARG_INDEX = {
8
+ "SetSortsForTaskRecorder:FilterManager": 0,
9
+ "ApplyFiltersForTaskRecorder:FilterManager": 0,
10
+ "ApplyFilterForTaskRecorder:QuickFilter": 2,
11
+ "ApplyFilters:FilterPane": 2,
12
+ };
13
+ /** Field names inside this family's embedded JSON that are AOT-defined structural metadata, not
14
+ * user-entered data -- confirmed against `mapping.ts`'s `FilterFieldMeta` shape and the QuickFilter
15
+ * JSON's own `FieldName`. Every other field (including any not explicitly recognized here, e.g.
16
+ * QuickFilter's `ReferenceUserActionSequenceId`) is redacted the same as any other recorded
17
+ * value, matching this file's "deliberately over-cautious" default -- only fields confirmed safe
18
+ * get an exception. */
19
+ const FILTER_FAMILY_SAFE_FIELDS = new Set(["DataSourceName", "FieldName", "Operator"]);
20
+ class Scrubber {
21
+ recordingFile;
22
+ recordingName;
23
+ tokenPrefix;
24
+ counter = 0;
25
+ values = [];
26
+ constructor(recordingFile, recordingName, tokenPrefix) {
27
+ this.recordingFile = recordingFile;
28
+ this.recordingName = recordingName;
29
+ this.tokenPrefix = tokenPrefix;
30
+ }
31
+ /** Redacts `value` in place if non-empty/non-null, returning the token (or the original null/""). */
32
+ redact(value, fieldPath, node) {
33
+ if (value === null || value === "")
34
+ return value;
35
+ this.counter += 1;
36
+ const token = `<<REDACTED:${this.tokenPrefix}:${this.counter}>>`;
37
+ this.values.push({
38
+ recordingFile: this.recordingFile,
39
+ recordingName: this.recordingName,
40
+ nodeKind: "kind" in node ? node.kind : "?",
41
+ nodeId: node.Id,
42
+ globalId: node.GlobalId ?? null,
43
+ sequence: node.Sequence,
44
+ fieldPath,
45
+ token,
46
+ originalValue: value,
47
+ });
48
+ return token;
49
+ }
50
+ /** Redacts every non-empty string leaf inside an unverified `Unknown.raw` payload, in place. */
51
+ redactDeep(obj, fieldPath, node) {
52
+ if (obj === null || obj === undefined)
53
+ return;
54
+ if (Array.isArray(obj)) {
55
+ obj.forEach((v, i) => this.redactDeepEntry(obj, i, `${fieldPath}[${i}]`, node));
56
+ return;
57
+ }
58
+ if (typeof obj === "object") {
59
+ for (const key of Object.keys(obj)) {
60
+ this.redactDeepEntry(obj, key, `${fieldPath}.${key}`, node);
61
+ }
62
+ }
63
+ }
64
+ redactDeepEntry(
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic walk over unverified raw XML JSON
66
+ container, key, fieldPath, node) {
67
+ const v = container[key];
68
+ if (typeof v === "string") {
69
+ if (v === "")
70
+ return;
71
+ container[key] = this.redact(v, fieldPath, node);
72
+ }
73
+ else if (typeof v === "object" && v !== null) {
74
+ this.redactDeep(v, fieldPath, node);
75
+ }
76
+ }
77
+ scrubNode(node) {
78
+ node.Description = this.redact(node.Description, `${node.kind}.Description`, node);
79
+ node.CustomDescription = this.redact(node.CustomDescription, `${node.kind}.CustomDescription`, node);
80
+ node.ScreenshotUri = this.redact(node.ScreenshotUri, `${node.kind}.ScreenshotUri`, node);
81
+ switch (node.kind) {
82
+ case "Scope":
83
+ node.Children = node.Children.map((c) => this.scrubNode(c));
84
+ return node;
85
+ case "MenuItemUserAction":
86
+ return node; // NavigationPath/MenuItemName/MenuItemType are structural, not data
87
+ case "CommandUserAction":
88
+ return this.scrubCommand(node);
89
+ case "PropertyUserAction":
90
+ node.Value = this.redact(node.Value, "PropertyUserAction.Value", node);
91
+ node.ValueLabel = this.redact(node.ValueLabel, "PropertyUserAction.ValueLabel", node);
92
+ return node;
93
+ case "Unknown":
94
+ this.redactDeep(node.raw, "Unknown.raw", node);
95
+ return node;
96
+ default:
97
+ return node;
98
+ }
99
+ }
100
+ scrubCommand(node) {
101
+ const jsonArgIndex = FILTER_FAMILY_JSON_ARG_INDEX[`${node.CommandName}:${node.ControlType}`];
102
+ node.Arguments = node.Arguments.map((arg, i) => ({
103
+ ...arg,
104
+ Value: i === jsonArgIndex
105
+ ? this.scrubFilterFamilyArgument(arg.Value, i, node)
106
+ : this.redact(arg.Value, `CommandUserAction.Arguments[${i}].Value`, node),
107
+ }));
108
+ node.ReturnValue = this.redact(node.ReturnValue, "CommandUserAction.ReturnValue", node);
109
+ return node;
110
+ }
111
+ /** Parses one FilterManager/QuickFilter/FilterPane argument's embedded JSON (a single object for
112
+ * QuickFilter, an array of them for the other three) and redacts only the value-bearing fields
113
+ * in place, leaving `FILTER_FAMILY_SAFE_FIELDS` visible so the re-serialized JSON is still
114
+ * parseable by `mapping.ts`'s `filterManagerHint`. Falls back to the normal full-string redaction
115
+ * if the value isn't valid JSON -- a malformed/unexpected shape should never accidentally leak
116
+ * a literal value just because it didn't parse the way this family usually does. */
117
+ scrubFilterFamilyArgument(rawValue, argIndex, node) {
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(rawValue);
121
+ }
122
+ catch {
123
+ return this.redact(rawValue, `CommandUserAction.Arguments[${argIndex}].Value`, node);
124
+ }
125
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
126
+ entries.forEach((entry, i) => {
127
+ if (entry !== null && typeof entry === "object") {
128
+ this.redactFilterFieldEntry(entry, `CommandUserAction.Arguments[${argIndex}].Value[${i}]`, node);
129
+ }
130
+ });
131
+ return JSON.stringify(parsed);
132
+ }
133
+ /** Redacts every field of one parsed filter-metadata object except `FILTER_FAMILY_SAFE_FIELDS`,
134
+ * in place -- including each element of a `Values` array, which is where the actual recorded
135
+ * filter/sort literal lives. */
136
+ redactFilterFieldEntry(entry, fieldPath, node) {
137
+ for (const key of Object.keys(entry)) {
138
+ if (FILTER_FAMILY_SAFE_FIELDS.has(key))
139
+ continue;
140
+ const value = entry[key];
141
+ if (typeof value === "string") {
142
+ entry[key] = this.redact(value, `${fieldPath}.${key}`, node);
143
+ }
144
+ else if (Array.isArray(value)) {
145
+ entry[key] = value.map((item, i) => typeof item === "string" ? this.redact(item, `${fieldPath}.${key}[${i}]`, node) : item);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ function sanitizeTokenPrefix(recordingFile) {
151
+ return recordingFile.replace(/\.axtr$/i, "").replace(/[^a-zA-Z0-9_-]+/g, "-").slice(0, 40);
152
+ }
153
+ /**
154
+ * Returns a deep copy of `recording` with every literal recorded value replaced by an opaque
155
+ * token, plus the list of stripped values (original text included) for local-only review.
156
+ */
157
+ export function scrubRecording(recording, recordingFile) {
158
+ const clone = structuredClone(recording);
159
+ const scrubber = new Scrubber(recordingFile, clone.Name, sanitizeTokenPrefix(recordingFile));
160
+ clone.Description = scrubber.redact(clone.Description, "Recording.Description", {
161
+ Id: clone.CanonicalId,
162
+ Sequence: 0,
163
+ });
164
+ clone.RootScope = scrubber.scrubNode(clone.RootScope);
165
+ return { recording: clone, values: scrubber.values };
166
+ }
@@ -0,0 +1,19 @@
1
+ import type { TestScript } from "./normalize.js";
2
+ import type { InputParamSpec } from "./testcase-schema.js";
3
+ export interface TestCaseInputDraft {
4
+ testScriptName: string;
5
+ candidates: InputParamSpec[];
6
+ }
7
+ /**
8
+ * Scans a normalized TestScript and drafts candidate InputParamSpecs for a human to review/edit
9
+ * before they become a TestCase's `inputs`. Every candidate traces back to either a
10
+ * recorded value (PropertyUserAction) or an ambiguity mapping.ts already flagged as low
11
+ * confidence -- nothing here is invented, so a human reviewing the draft always has the
12
+ * original recorded context (`example`/`description`) to work from.
13
+ *
14
+ * CAUTION: `example` is copied verbatim from the source .axtr (a real account number, journal
15
+ * name, amount, etc. from whatever environment it was recorded against). If the source
16
+ * recording contains real business data, review/scrub `example` values before committing the
17
+ * resulting testcases/*.json to a shared or public repo.
18
+ */
19
+ export declare function suggestInputs(script: TestScript): TestCaseInputDraft;
@@ -0,0 +1,92 @@
1
+ import { mapStep } from "./mapping.js";
2
+ function uniqueName(base, seen) {
3
+ if (!seen.has(base)) {
4
+ seen.add(base);
5
+ return base;
6
+ }
7
+ let n = 2;
8
+ while (seen.has(`${base}_${n}`))
9
+ n++;
10
+ const name = `${base}_${n}`;
11
+ seen.add(name);
12
+ return name;
13
+ }
14
+ function draftFromPropertyStep(step, seen) {
15
+ if (step.node.kind !== "PropertyUserAction")
16
+ return null;
17
+ const controlName = step.node.ControlName || `step${step.sequence}`;
18
+ const name = uniqueName(controlName, seen);
19
+ return {
20
+ name,
21
+ // ControlLabel is the human-readable label D365FO itself shows next to the field (e.g.
22
+ // "Quantity"), as opposed to `name`/controlName which is the internal control identifier
23
+ // (e.g. "PurchLine_PurchQtyGrid") -- useful for UIs that want to show a form-goer-friendly
24
+ // label instead of the raw control name.
25
+ controlLabel: step.node.ControlLabel || undefined,
26
+ description: step.description || `Value for ${controlName}`,
27
+ example: step.node.Value,
28
+ required: true,
29
+ bindTo: { kind: "control", stepSequence: step.sequence, controlName },
30
+ };
31
+ }
32
+ /**
33
+ * Grid row selection is usually recorded as a pair of steps (e.g. ChangeSelectedIndexInCache ->
34
+ * form_filter_grid, then NavigationAction/MarkActiveRow -> form_select_grid_row). Only the
35
+ * filter step needs a fresh business-key input; the follow-up select step just acts on whatever
36
+ * the filter resolved to, so drafting a second candidate for it would be redundant.
37
+ *
38
+ * Any `form_filter_grid` hint qualifies, regardless of confidence -- confirmed live as a real gap
39
+ * (not just theoretical) on "Confirm purchase order.axtr": its QuickFilter step
40
+ * (ApplyFiltersForTaskRecorder) resolves through `filterManagerHint` at `confidence: "medium"`,
41
+ * with a concrete recorded gridColumnValue (e.g. "000039", one specific purchase order from
42
+ * whatever environment the recording was made in) -- but "medium" there describes confidence in
43
+ * the replay *mechanics* (which grid column, which tool), not confidence that the recorded value
44
+ * is safe to hardcode. It's exactly as environment-specific as the low-confidence row-position
45
+ * case; only checking for `confidence === "low"` silently stopped drafting a candidate for this
46
+ * family the moment mapping.ts got better at the former without changing the latter.
47
+ */
48
+ function draftFromGridFilterStep(step, seen) {
49
+ const hint = mapStep(step);
50
+ if (!hint || hint.tool !== "form_filter_grid")
51
+ return null;
52
+ const recordedValue = typeof hint.params.gridColumnValue === "string" ? hint.params.gridColumnValue : undefined;
53
+ const name = uniqueName(`record_${step.sequence}`, seen);
54
+ return {
55
+ name,
56
+ description: `Needs a business key to identify the record for: "${step.description}". ` +
57
+ (recordedValue
58
+ ? `The recording filtered by "${recordedValue}", one specific record from that session -- `
59
+ : "The source recording only captured a row position, which is not portable across environments/data -- ") +
60
+ "supply the actual value (e.g. an account number, order id) for this environment/data here.",
61
+ required: true,
62
+ example: recordedValue,
63
+ bindTo: { kind: "grid-row-selector", stepSequence: step.sequence, note: step.description },
64
+ };
65
+ }
66
+ /**
67
+ * Scans a normalized TestScript and drafts candidate InputParamSpecs for a human to review/edit
68
+ * before they become a TestCase's `inputs`. Every candidate traces back to either a
69
+ * recorded value (PropertyUserAction) or an ambiguity mapping.ts already flagged as low
70
+ * confidence -- nothing here is invented, so a human reviewing the draft always has the
71
+ * original recorded context (`example`/`description`) to work from.
72
+ *
73
+ * CAUTION: `example` is copied verbatim from the source .axtr (a real account number, journal
74
+ * name, amount, etc. from whatever environment it was recorded against). If the source
75
+ * recording contains real business data, review/scrub `example` values before committing the
76
+ * resulting testcases/*.json to a shared or public repo.
77
+ */
78
+ export function suggestInputs(script) {
79
+ const seen = new Set();
80
+ const candidates = [];
81
+ for (const step of script.steps) {
82
+ const fromProperty = draftFromPropertyStep(step, seen);
83
+ if (fromProperty) {
84
+ candidates.push(fromProperty);
85
+ continue;
86
+ }
87
+ const fromGridFilter = draftFromGridFilterStep(step, seen);
88
+ if (fromGridFilter)
89
+ candidates.push(fromGridFilter);
90
+ }
91
+ return { testScriptName: script.name, candidates };
92
+ }
@@ -0,0 +1,59 @@
1
+ import type { TestSuite, TestSuiteEntry, RunCondition } from "./suite-schema.js";
2
+ import type { TestCase } from "./testcase-schema.js";
3
+ import type { Fixture } from "./fixture-schema.js";
4
+ import { type TestRun } from "./report-schema.js";
5
+ /**
6
+ * Kahn's algorithm over `dependsOn` edges. Throws on an unknown dependsOn id or a cycle --
7
+ * these must never be left to LLM judgment, unlike everything in mapping.ts's hints.
8
+ */
9
+ export declare function topologicalOrder(suite: TestSuite): string[];
10
+ /** Resolves a test case's declared `inputs` to concrete values for one suite run, following
11
+ * each `inputBindings` entry to a fixture, an earlier test case's captured output, or a
12
+ * literal. Throws a clear, specific error rather than silently defaulting -- a missing or
13
+ * not-yet-produced value should stop the run, not get guessed by the LLM. */
14
+ export declare function resolveInputs(entry: TestSuiteEntry, testCase: TestCase, testRun: TestRun, fixtures: Fixture[]): Record<string, string>;
15
+ /** Evaluates a `runIf` gate against the run so far. `condition.testCaseId` is guaranteed (by
16
+ * `topologicalOrder`'s validation) to be one of the entry's own `dependsOn`, so by the time this
17
+ * is called that upstream test case has already passed -- this only ever reads its captured
18
+ * *output*, never its status (see `RunCondition`'s doc comment in `suite-schema.ts` for why a
19
+ * status check would be vacuous here). An upstream that didn't capture the referenced output at
20
+ * all evaluates to false (skip), the same "missing means doesn't match" behavior a real value
21
+ * comparison would need anyway. */
22
+ export declare function evaluateRunCondition(condition: RunCondition, testRun: TestRun): boolean;
23
+ export type SuiteProgress = {
24
+ status: "runnable";
25
+ testCaseIds: string[];
26
+ } | {
27
+ status: "newly-blocked";
28
+ blockedIds: string[];
29
+ } | {
30
+ status: "newly-skipped";
31
+ skippedIds: string[];
32
+ } | {
33
+ status: "waiting";
34
+ } | {
35
+ status: "complete";
36
+ };
37
+ /**
38
+ * Computes the next action for a suite run given its current TestRun:
39
+ * - If any pending entry now has a failed/blocked/skipped dependency, report it as newly-blocked
40
+ * so the caller can record that status before asking again (propagation is explicit, not
41
+ * silent) -- a skipped dependency counts the same as a failed one here, since nothing downstream
42
+ * of it can ever satisfy "every dependency passed" otherwise, which would leave it stuck waiting
43
+ * forever instead of reaching a terminal status.
44
+ * - Otherwise, if any pending-and-otherwise-runnable entry's `runIf` evaluates false, report it as
45
+ * newly-skipped the same way, before considering anything runnable.
46
+ * - Otherwise return every pending entry (topological order, for determinism) that's runnable and
47
+ * (if it has one) passes its `runIf` -- usually just one, but a suite whose DAG has two or more
48
+ * entries that share a satisfied dependency without depending on each other (e.g. two entries
49
+ * that both only depend on a common upstream entry) surfaces all of them at once here. `nextStep`
50
+ * itself makes no scheduling decision beyond exposing what's currently unblocked -- whether the
51
+ * caller works through `testCaseIds` one at a time or hands them to concurrent workers is
52
+ * entirely the caller's call; nothing here assumes either.
53
+ * - Otherwise, if every entry has a terminal status, the suite is complete.
54
+ * - Otherwise ("waiting"): nothing is pending-and-runnable right now, but something is still
55
+ * `"running"` -- a normal mid-flight state, not an error. A root entry (no dependencies) is
56
+ * always immediately runnable once pending, so this can only happen when every not-yet-terminal
57
+ * entry is already `"running"`.
58
+ */
59
+ export declare function nextStep(suite: TestSuite, testRun: TestRun): SuiteProgress;