newo 3.7.2 → 3.7.3
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 +13 -1
- package/README.md +24 -0
- package/dist/sync/json-attr-utils.d.ts +40 -11
- package/dist/sync/json-attr-utils.js +89 -22
- package/package.json +1 -1
- package/src/sync/json-attr-utils.ts +84 -19
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.7.3] - 2026-05-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Workflow Builder canvas blank-screen, take two** - JSON-typed attribute values that contain Markdown body text with `\_` escapes (Builder uses backslash-underscore to escape underscores) and/or structural newlines from pretty-printed JSON no longer corrupt the canvas on push. Two bugs were leaking through v3.7.1's STRING-coercion fix: (a) `yaml.dump` of a pretty-printed JSON string emitted a double-quoted scalar with `\n` escape sequences, which `patchYamlToPyyaml` then rewrote as a single-quoted scalar where `\n` became two literal chars - Builder's `JSON.parse` then choked on backslash-n as structural whitespace; (b) `\_` is not a valid JSON escape per RFC 8259 and Chrome V8's `JSON.parse` throws on it, silently blanking the Builder. Fix: `normalizeJsonValueForStorage` now strips invalid escape sequences via a quote-aware walker (`fixInvalidJsonEscapes`), then compacts via `JSON.parse` + `JSON.stringify` so the value is a single-line string that survives `yaml.dump` -> `patchYamlToPyyaml` without escape-sequence corruption. Existing pretty-printed canvases in `attributes.yaml` will be reformatted to compact form on next pull (one-time stylistic diff, no semantic loss). Change-detection (`jsonValuesEqual`) continues to canonicalize both sides so the new compact form does not trigger spurious pushes against remotes that may still be pretty. Reported by Bob in [#7](https://github.com/sabbah13/newo-cli/pull/7); 25 regression tests in `test/json-attribute-roundtrip.test.js` covering both bug families and the full pull -> dump -> patch -> reload -> `JSON.parse` pipeline.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`fixInvalidJsonEscapes(s)`** in `src/sync/json-attr-utils.ts` - quote-aware walker that strips invalid JSON escape sequences (`\_`, `\.`, etc.) inside JSON string values per RFC 8259. Preserves all valid escapes (`\" \\ \/ \b \f \n \r \t \uXXXX`) and structural characters outside strings.
|
|
19
|
+
|
|
10
20
|
## [3.7.2] - 2026-05-17
|
|
11
21
|
|
|
12
22
|
### Fixed
|
|
@@ -1051,7 +1061,9 @@ Another Item: $Price [Modifiers: modifier3]
|
|
|
1051
1061
|
- GitHub Actions CI/CD integration
|
|
1052
1062
|
- Robust authentication with token refresh
|
|
1053
1063
|
|
|
1054
|
-
[Unreleased]: https://github.com/sabbah13/newo-cli/compare/v3.7.
|
|
1064
|
+
[Unreleased]: https://github.com/sabbah13/newo-cli/compare/v3.7.3...HEAD
|
|
1065
|
+
[3.7.3]: https://github.com/sabbah13/newo-cli/compare/v3.7.2...v3.7.3
|
|
1066
|
+
[3.7.2]: https://github.com/sabbah13/newo-cli/compare/v3.7.1...v3.7.2
|
|
1055
1067
|
[3.7.1]: https://github.com/sabbah13/newo-cli/compare/v3.7.0...v3.7.1
|
|
1056
1068
|
[3.3.0]: https://github.com/sabbah13/newo-cli/compare/v3.2.0...v3.3.0
|
|
1057
1069
|
[3.2.0]: https://github.com/sabbah13/newo-cli/compare/v3.1.0...v3.2.0
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
**NEWO CLI** - Professional command-line tool for NEWO AI Agent development. Features **modular architecture**, **IDN-based file management**, and **comprehensive multi-customer support**.
|
|
9
9
|
|
|
10
10
|
Sync NEWO "Project → Agent → Flow → Skills" structure to local files with:
|
|
11
|
+
- 🆕 **Canvas blank-screen hardening** (v3.7.3) - JSON-typed attributes (e.g. Workflow Builder canvas) with Markdown `\_` escapes or structural newlines no longer corrupt the canvas on push ([#7](https://github.com/sabbah13/newo-cli/pull/7))
|
|
12
|
+
- 🆕 **Flow metadata sync** (v3.7.2) - `newo push` now reconciles flow title, events, and state_fields from local `metadata.yaml` to the platform (closes [#3](https://github.com/sabbah13/newo-cli/issues/3))
|
|
11
13
|
- 🆕 **Dual format support** (v3.6.0) - `cli_v1` (native) and `newo_v2` (platform compatible), auto-detected per customer
|
|
12
14
|
- 🆕 **Libraries** (v3.6.0) - Pull/push shared reusable skills across agents within a project
|
|
13
15
|
- 🆕 **Bulk export** (v3.6.0) - `newo export` downloads complete V2 ZIP from platform
|
|
@@ -153,6 +155,28 @@ NEWO_REFRESH_URL=custom_refresh_endpoint # Custom refresh endpoint
|
|
|
153
155
|
| `newo import-akb` | Import knowledge base articles | • Structured text parsing<br>• Bulk article import<br>• Validation and error reporting |
|
|
154
156
|
| `newo meta` | Get project metadata (debug) | • Project structure analysis<br>• Metadata validation |
|
|
155
157
|
|
|
158
|
+
### Flow Metadata Sync (NEW v3.7.2)
|
|
159
|
+
|
|
160
|
+
`newo push` now reconciles **flow-level metadata** — title, `events:`, and `state_fields:` — from local YAML to the platform. Before v3.7.2 push only uploaded skill scripts, so edits to a flow's `metadata.yaml` (V1) or `{FlowIdn}.yaml` (V2) silently never reached the platform; events added via `newo create-event` could appear to disappear after a pull → push cycle. Closes [#3](https://github.com/sabbah13/newo-cli/issues/3).
|
|
161
|
+
|
|
162
|
+
**How it works:**
|
|
163
|
+
|
|
164
|
+
| Local change in `metadata.yaml` | What push does |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `title:` changed | `PATCH /api/v1/designer/flows/{id}` with full descriptor |
|
|
167
|
+
| New event in `events:` | `POST /api/v1/designer/flows/{id}/events` |
|
|
168
|
+
| Event field edited | `PATCH /api/v1/designer/flows/events/{eventId}` |
|
|
169
|
+
| Event removed from list | `DELETE /api/v1/designer/flows/events/{eventId}` |
|
|
170
|
+
| Same for `state_fields:` | Mirrored CRUD against `/states` |
|
|
171
|
+
|
|
172
|
+
**Safety:**
|
|
173
|
+
|
|
174
|
+
- **Hash-gated**: flows whose `metadata.yaml` SHA256 is unchanged are *not* compared against the platform. Stale local trees cannot wipe events you created via the Builder UI.
|
|
175
|
+
- **Full sync on changed flows**: when you *do* edit `metadata.yaml`, local becomes the source of truth for that flow. Events present on the platform but missing locally are deleted. To keep events created out-of-band, run `newo pull` before editing.
|
|
176
|
+
- **Push output**: changed flows print `↑ Flow <flow>: events +N/~N/-N, states +N/~N/-N` so you see exactly what synced.
|
|
177
|
+
|
|
178
|
+
**Available in:** legacy V1 push path (`pushChanged`), `ProjectSyncStrategy`, and `V2ProjectSyncStrategy`. Works the same with `newo push --format cli_v1` and `--format newo_v2`.
|
|
179
|
+
|
|
156
180
|
### Lint, Format, Check (NEW v3.7.0)
|
|
157
181
|
|
|
158
182
|
Static-analysis over DSL files, powered by [`newo-dsl-analyzer`](https://www.npmjs.com/package/newo-dsl-analyzer). Same engine that runs in the VS Code extension - no drift between editor and CI.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `value_type: json`. The API may return the `value` field as either a
|
|
9
9
|
* STRING containing JSON or as an already-parsed OBJECT.
|
|
10
10
|
*
|
|
11
|
-
* Without normalization,
|
|
11
|
+
* Without normalization, several bugs leak through:
|
|
12
12
|
*
|
|
13
13
|
* 1. When the API returns the value as an OBJECT, `yaml.dump` serializes
|
|
14
14
|
* it as a YAML structure (mappings/sequences). Pushing back then sends
|
|
@@ -21,10 +21,25 @@
|
|
|
21
21
|
* string vs object representations it triggers spurious pushes that
|
|
22
22
|
* overwrite the canvas with the wrong shape (Builder shows blank).
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* 3. (Bug 3.7.2-a) Canvas JSON strings with structural newlines (real
|
|
25
|
+
* U+000A between tokens) can be emitted by yaml.dump as double-quoted
|
|
26
|
+
* scalars with `\n` escape sequences. patchYamlToPyyaml then converts
|
|
27
|
+
* those to single-quoted YAML scalars, where `\n` is treated as two
|
|
28
|
+
* literal chars (backslash + n). On push the platform stores those
|
|
29
|
+
* literal chars and the Builder calls JSON.parse, which fails on
|
|
30
|
+
* backslash-n as structural whitespace.
|
|
31
|
+
*
|
|
32
|
+
* 4. (Bug 3.7.2-b) Canvas body text contains Markdown with `\_`
|
|
33
|
+
* (backslash + underscore). `\_` is not a valid JSON escape sequence
|
|
34
|
+
* per RFC 8259 (valid ones: " \ / b f n r t uXXXX). Chrome V8's
|
|
35
|
+
* JSON.parse is strict: it throws SyntaxError on `\_`, silently
|
|
36
|
+
* blanking the Builder.
|
|
37
|
+
*
|
|
38
|
+
* The fix for (3) and (4): for `value_type: json` string values, strip
|
|
39
|
+
* invalid escape sequences then compact via JSON.parse + JSON.stringify.
|
|
40
|
+
* Compaction removes structural newlines and re-serializes all string
|
|
41
|
+
* values with only valid JSON escapes, producing a single-line string
|
|
42
|
+
* that round-trips through YAML without corruption.
|
|
28
43
|
*/
|
|
29
44
|
/**
|
|
30
45
|
* True if the attribute is a JSON-typed attribute (case- and
|
|
@@ -32,26 +47,40 @@
|
|
|
32
47
|
* `ValueType.JSON`, etc.).
|
|
33
48
|
*/
|
|
34
49
|
export declare function isJsonValueType(valueType: unknown): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Fix invalid JSON escape sequences inside JSON string values.
|
|
52
|
+
*
|
|
53
|
+
* Per RFC 8259, valid escape sequences inside a JSON string are:
|
|
54
|
+
* \" \\ \/ \b \f \n \r \t \uXXXX
|
|
55
|
+
* Anything else (e.g. `\_` `\.` from Markdown) is invalid and causes
|
|
56
|
+
* JSON.parse to throw. Fix: drop the backslash (e.g. `\_` → `_`).
|
|
57
|
+
*
|
|
58
|
+
* Only modifies characters inside JSON string values (tracks quote
|
|
59
|
+
* context). Structural characters outside strings are untouched.
|
|
60
|
+
*/
|
|
61
|
+
export declare function fixInvalidJsonEscapes(s: string): string;
|
|
35
62
|
/**
|
|
36
63
|
* Coerce a JSON-typed attribute's value to a STRING suitable for storage
|
|
37
64
|
* in attributes.yaml and for sending to the platform.
|
|
38
65
|
*
|
|
39
66
|
* - `null` / `undefined` → `''`
|
|
40
67
|
* - object → compact JSON string (`JSON.stringify(value)`)
|
|
41
|
-
* - string →
|
|
68
|
+
* - string → fix invalid escapes (e.g. `\_` → `_`), then compact via
|
|
69
|
+
* JSON.parse + JSON.stringify. If parsing still fails after
|
|
70
|
+
* fixing escapes, return the fixed string as-is.
|
|
42
71
|
* - other → `String(value)`
|
|
43
72
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
73
|
+
* Compacting removes structural newlines and guarantees a single-line
|
|
74
|
+
* string that yaml.dump serializes without escape-sequence corruption in
|
|
75
|
+
* the patchYamlToPyyaml pass. See module-level comment for full context.
|
|
47
76
|
*/
|
|
48
77
|
export declare function normalizeJsonValueForStorage(value: unknown): string;
|
|
49
78
|
/**
|
|
50
79
|
* Canonical comparison for JSON-typed attribute values.
|
|
51
80
|
*
|
|
52
81
|
* Returns the canonical form (compact JSON if parseable, otherwise the
|
|
53
|
-
*
|
|
54
|
-
* compact-printed JSON does not register as a change, and so that an
|
|
82
|
+
* fixed string). Use this on both sides of a comparison so that pretty-
|
|
83
|
+
* vs compact-printed JSON does not register as a change, and so that an
|
|
55
84
|
* object on one side equals its stringified form on the other side.
|
|
56
85
|
*/
|
|
57
86
|
export declare function canonicalJsonValue(value: unknown): string;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `value_type: json`. The API may return the `value` field as either a
|
|
9
9
|
* STRING containing JSON or as an already-parsed OBJECT.
|
|
10
10
|
*
|
|
11
|
-
* Without normalization,
|
|
11
|
+
* Without normalization, several bugs leak through:
|
|
12
12
|
*
|
|
13
13
|
* 1. When the API returns the value as an OBJECT, `yaml.dump` serializes
|
|
14
14
|
* it as a YAML structure (mappings/sequences). Pushing back then sends
|
|
@@ -21,10 +21,25 @@
|
|
|
21
21
|
* string vs object representations it triggers spurious pushes that
|
|
22
22
|
* overwrite the canvas with the wrong shape (Builder shows blank).
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* 3. (Bug 3.7.2-a) Canvas JSON strings with structural newlines (real
|
|
25
|
+
* U+000A between tokens) can be emitted by yaml.dump as double-quoted
|
|
26
|
+
* scalars with `\n` escape sequences. patchYamlToPyyaml then converts
|
|
27
|
+
* those to single-quoted YAML scalars, where `\n` is treated as two
|
|
28
|
+
* literal chars (backslash + n). On push the platform stores those
|
|
29
|
+
* literal chars and the Builder calls JSON.parse, which fails on
|
|
30
|
+
* backslash-n as structural whitespace.
|
|
31
|
+
*
|
|
32
|
+
* 4. (Bug 3.7.2-b) Canvas body text contains Markdown with `\_`
|
|
33
|
+
* (backslash + underscore). `\_` is not a valid JSON escape sequence
|
|
34
|
+
* per RFC 8259 (valid ones: " \ / b f n r t uXXXX). Chrome V8's
|
|
35
|
+
* JSON.parse is strict: it throws SyntaxError on `\_`, silently
|
|
36
|
+
* blanking the Builder.
|
|
37
|
+
*
|
|
38
|
+
* The fix for (3) and (4): for `value_type: json` string values, strip
|
|
39
|
+
* invalid escape sequences then compact via JSON.parse + JSON.stringify.
|
|
40
|
+
* Compaction removes structural newlines and re-serializes all string
|
|
41
|
+
* values with only valid JSON escapes, producing a single-line string
|
|
42
|
+
* that round-trips through YAML without corruption.
|
|
28
43
|
*/
|
|
29
44
|
/**
|
|
30
45
|
* True if the attribute is a JSON-typed attribute (case- and
|
|
@@ -37,24 +52,84 @@ export function isJsonValueType(valueType) {
|
|
|
37
52
|
const lower = valueType.toLowerCase();
|
|
38
53
|
return lower === 'json' || lower.endsWith('.json');
|
|
39
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Fix invalid JSON escape sequences inside JSON string values.
|
|
57
|
+
*
|
|
58
|
+
* Per RFC 8259, valid escape sequences inside a JSON string are:
|
|
59
|
+
* \" \\ \/ \b \f \n \r \t \uXXXX
|
|
60
|
+
* Anything else (e.g. `\_` `\.` from Markdown) is invalid and causes
|
|
61
|
+
* JSON.parse to throw. Fix: drop the backslash (e.g. `\_` → `_`).
|
|
62
|
+
*
|
|
63
|
+
* Only modifies characters inside JSON string values (tracks quote
|
|
64
|
+
* context). Structural characters outside strings are untouched.
|
|
65
|
+
*/
|
|
66
|
+
export function fixInvalidJsonEscapes(s) {
|
|
67
|
+
const VALID_ESCAPES = new Set(['"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u']);
|
|
68
|
+
const result = [];
|
|
69
|
+
let inString = false;
|
|
70
|
+
let i = 0;
|
|
71
|
+
while (i < s.length) {
|
|
72
|
+
const c = s[i];
|
|
73
|
+
if (inString) {
|
|
74
|
+
if (c === '\\' && i + 1 < s.length) {
|
|
75
|
+
const next = s[i + 1];
|
|
76
|
+
if (VALID_ESCAPES.has(next)) {
|
|
77
|
+
result.push(c, next);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result.push(next); // drop the backslash — \_ → _, etc.
|
|
81
|
+
}
|
|
82
|
+
i += 2;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
else if (c === '"') {
|
|
86
|
+
inString = false;
|
|
87
|
+
result.push(c);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
result.push(c);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
if (c === '"') {
|
|
95
|
+
inString = true;
|
|
96
|
+
result.push(c);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
result.push(c);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
return result.join('');
|
|
105
|
+
}
|
|
40
106
|
/**
|
|
41
107
|
* Coerce a JSON-typed attribute's value to a STRING suitable for storage
|
|
42
108
|
* in attributes.yaml and for sending to the platform.
|
|
43
109
|
*
|
|
44
110
|
* - `null` / `undefined` → `''`
|
|
45
111
|
* - object → compact JSON string (`JSON.stringify(value)`)
|
|
46
|
-
* - string →
|
|
112
|
+
* - string → fix invalid escapes (e.g. `\_` → `_`), then compact via
|
|
113
|
+
* JSON.parse + JSON.stringify. If parsing still fails after
|
|
114
|
+
* fixing escapes, return the fixed string as-is.
|
|
47
115
|
* - other → `String(value)`
|
|
48
116
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
117
|
+
* Compacting removes structural newlines and guarantees a single-line
|
|
118
|
+
* string that yaml.dump serializes without escape-sequence corruption in
|
|
119
|
+
* the patchYamlToPyyaml pass. See module-level comment for full context.
|
|
52
120
|
*/
|
|
53
121
|
export function normalizeJsonValueForStorage(value) {
|
|
54
122
|
if (value == null)
|
|
55
123
|
return '';
|
|
56
|
-
if (typeof value === 'string')
|
|
57
|
-
|
|
124
|
+
if (typeof value === 'string') {
|
|
125
|
+
const fixed = fixInvalidJsonEscapes(value);
|
|
126
|
+
try {
|
|
127
|
+
return JSON.stringify(JSON.parse(fixed));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return fixed;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
58
133
|
if (typeof value === 'object') {
|
|
59
134
|
try {
|
|
60
135
|
return JSON.stringify(value);
|
|
@@ -69,20 +144,12 @@ export function normalizeJsonValueForStorage(value) {
|
|
|
69
144
|
* Canonical comparison for JSON-typed attribute values.
|
|
70
145
|
*
|
|
71
146
|
* Returns the canonical form (compact JSON if parseable, otherwise the
|
|
72
|
-
*
|
|
73
|
-
* compact-printed JSON does not register as a change, and so that an
|
|
147
|
+
* fixed string). Use this on both sides of a comparison so that pretty-
|
|
148
|
+
* vs compact-printed JSON does not register as a change, and so that an
|
|
74
149
|
* object on one side equals its stringified form on the other side.
|
|
75
150
|
*/
|
|
76
151
|
export function canonicalJsonValue(value) {
|
|
77
|
-
|
|
78
|
-
if (stringified === '')
|
|
79
|
-
return '';
|
|
80
|
-
try {
|
|
81
|
-
return JSON.stringify(JSON.parse(stringified));
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return stringified;
|
|
85
|
-
}
|
|
152
|
+
return normalizeJsonValueForStorage(value);
|
|
86
153
|
}
|
|
87
154
|
/**
|
|
88
155
|
* True if two JSON-typed attribute values are semantically equal.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.3",
|
|
4
4
|
"description": "NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features account migration, integration management, webhook automation, AKB knowledge base, project attributes, sandbox testing, IDN-based file management, real-time progress tracking, intelligent sync operations, and comprehensive multi-customer support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `value_type: json`. The API may return the `value` field as either a
|
|
9
9
|
* STRING containing JSON or as an already-parsed OBJECT.
|
|
10
10
|
*
|
|
11
|
-
* Without normalization,
|
|
11
|
+
* Without normalization, several bugs leak through:
|
|
12
12
|
*
|
|
13
13
|
* 1. When the API returns the value as an OBJECT, `yaml.dump` serializes
|
|
14
14
|
* it as a YAML structure (mappings/sequences). Pushing back then sends
|
|
@@ -21,10 +21,25 @@
|
|
|
21
21
|
* string vs object representations it triggers spurious pushes that
|
|
22
22
|
* overwrite the canvas with the wrong shape (Builder shows blank).
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* 3. (Bug 3.7.2-a) Canvas JSON strings with structural newlines (real
|
|
25
|
+
* U+000A between tokens) can be emitted by yaml.dump as double-quoted
|
|
26
|
+
* scalars with `\n` escape sequences. patchYamlToPyyaml then converts
|
|
27
|
+
* those to single-quoted YAML scalars, where `\n` is treated as two
|
|
28
|
+
* literal chars (backslash + n). On push the platform stores those
|
|
29
|
+
* literal chars and the Builder calls JSON.parse, which fails on
|
|
30
|
+
* backslash-n as structural whitespace.
|
|
31
|
+
*
|
|
32
|
+
* 4. (Bug 3.7.2-b) Canvas body text contains Markdown with `\_`
|
|
33
|
+
* (backslash + underscore). `\_` is not a valid JSON escape sequence
|
|
34
|
+
* per RFC 8259 (valid ones: " \ / b f n r t uXXXX). Chrome V8's
|
|
35
|
+
* JSON.parse is strict: it throws SyntaxError on `\_`, silently
|
|
36
|
+
* blanking the Builder.
|
|
37
|
+
*
|
|
38
|
+
* The fix for (3) and (4): for `value_type: json` string values, strip
|
|
39
|
+
* invalid escape sequences then compact via JSON.parse + JSON.stringify.
|
|
40
|
+
* Compaction removes structural newlines and re-serializes all string
|
|
41
|
+
* values with only valid JSON escapes, producing a single-line string
|
|
42
|
+
* that round-trips through YAML without corruption.
|
|
28
43
|
*/
|
|
29
44
|
|
|
30
45
|
/**
|
|
@@ -38,22 +53,78 @@ export function isJsonValueType(valueType: unknown): boolean {
|
|
|
38
53
|
return lower === 'json' || lower.endsWith('.json');
|
|
39
54
|
}
|
|
40
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Fix invalid JSON escape sequences inside JSON string values.
|
|
58
|
+
*
|
|
59
|
+
* Per RFC 8259, valid escape sequences inside a JSON string are:
|
|
60
|
+
* \" \\ \/ \b \f \n \r \t \uXXXX
|
|
61
|
+
* Anything else (e.g. `\_` `\.` from Markdown) is invalid and causes
|
|
62
|
+
* JSON.parse to throw. Fix: drop the backslash (e.g. `\_` → `_`).
|
|
63
|
+
*
|
|
64
|
+
* Only modifies characters inside JSON string values (tracks quote
|
|
65
|
+
* context). Structural characters outside strings are untouched.
|
|
66
|
+
*/
|
|
67
|
+
export function fixInvalidJsonEscapes(s: string): string {
|
|
68
|
+
const VALID_ESCAPES = new Set(['"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u']);
|
|
69
|
+
const result: string[] = [];
|
|
70
|
+
let inString = false;
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (i < s.length) {
|
|
73
|
+
const c = s[i]!;
|
|
74
|
+
if (inString) {
|
|
75
|
+
if (c === '\\' && i + 1 < s.length) {
|
|
76
|
+
const next = s[i + 1]!;
|
|
77
|
+
if (VALID_ESCAPES.has(next)) {
|
|
78
|
+
result.push(c, next);
|
|
79
|
+
} else {
|
|
80
|
+
result.push(next); // drop the backslash — \_ → _, etc.
|
|
81
|
+
}
|
|
82
|
+
i += 2;
|
|
83
|
+
continue;
|
|
84
|
+
} else if (c === '"') {
|
|
85
|
+
inString = false;
|
|
86
|
+
result.push(c);
|
|
87
|
+
} else {
|
|
88
|
+
result.push(c);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
if (c === '"') {
|
|
92
|
+
inString = true;
|
|
93
|
+
result.push(c);
|
|
94
|
+
} else {
|
|
95
|
+
result.push(c);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
return result.join('');
|
|
101
|
+
}
|
|
102
|
+
|
|
41
103
|
/**
|
|
42
104
|
* Coerce a JSON-typed attribute's value to a STRING suitable for storage
|
|
43
105
|
* in attributes.yaml and for sending to the platform.
|
|
44
106
|
*
|
|
45
107
|
* - `null` / `undefined` → `''`
|
|
46
108
|
* - object → compact JSON string (`JSON.stringify(value)`)
|
|
47
|
-
* - string →
|
|
109
|
+
* - string → fix invalid escapes (e.g. `\_` → `_`), then compact via
|
|
110
|
+
* JSON.parse + JSON.stringify. If parsing still fails after
|
|
111
|
+
* fixing escapes, return the fixed string as-is.
|
|
48
112
|
* - other → `String(value)`
|
|
49
113
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
114
|
+
* Compacting removes structural newlines and guarantees a single-line
|
|
115
|
+
* string that yaml.dump serializes without escape-sequence corruption in
|
|
116
|
+
* the patchYamlToPyyaml pass. See module-level comment for full context.
|
|
53
117
|
*/
|
|
54
118
|
export function normalizeJsonValueForStorage(value: unknown): string {
|
|
55
119
|
if (value == null) return '';
|
|
56
|
-
if (typeof value === 'string')
|
|
120
|
+
if (typeof value === 'string') {
|
|
121
|
+
const fixed = fixInvalidJsonEscapes(value);
|
|
122
|
+
try {
|
|
123
|
+
return JSON.stringify(JSON.parse(fixed));
|
|
124
|
+
} catch {
|
|
125
|
+
return fixed;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
57
128
|
if (typeof value === 'object') {
|
|
58
129
|
try {
|
|
59
130
|
return JSON.stringify(value);
|
|
@@ -68,18 +139,12 @@ export function normalizeJsonValueForStorage(value: unknown): string {
|
|
|
68
139
|
* Canonical comparison for JSON-typed attribute values.
|
|
69
140
|
*
|
|
70
141
|
* Returns the canonical form (compact JSON if parseable, otherwise the
|
|
71
|
-
*
|
|
72
|
-
* compact-printed JSON does not register as a change, and so that an
|
|
142
|
+
* fixed string). Use this on both sides of a comparison so that pretty-
|
|
143
|
+
* vs compact-printed JSON does not register as a change, and so that an
|
|
73
144
|
* object on one side equals its stringified form on the other side.
|
|
74
145
|
*/
|
|
75
146
|
export function canonicalJsonValue(value: unknown): string {
|
|
76
|
-
|
|
77
|
-
if (stringified === '') return '';
|
|
78
|
-
try {
|
|
79
|
-
return JSON.stringify(JSON.parse(stringified));
|
|
80
|
-
} catch {
|
|
81
|
-
return stringified;
|
|
82
|
-
}
|
|
147
|
+
return normalizeJsonValueForStorage(value);
|
|
83
148
|
}
|
|
84
149
|
|
|
85
150
|
/**
|