@indigoai-us/hq-cli 5.345.128 → 5.345.130
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 +22 -0
- package/assets/scaffold/core/scripts/lib/codex-output-schema.mjs +304 -0
- package/assets/scaffold/core/scripts/workflow-runner.mjs +2890 -0
- package/dist/command-catalog.generated.d.ts +29 -0
- package/dist/command-catalog.generated.js +34 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +8 -2
- package/dist/commands/core-pipeline-workflow-runner-native.d.ts +175 -0
- package/dist/commands/core-pipeline-workflow-runner-native.js +3237 -0
- package/dist/commands/core-workflow-run-command.d.ts +4 -0
- package/dist/commands/core-workflow-run-command.js +41 -0
- package/dist/commands/core.js +4 -0
- package/dist/commands/daemon.js +2 -0
- package/dist/commands/lanes-accounts.js +0 -1
- package/dist/commands/lanes.js +26 -14
- package/dist/commands/mesh.js +32 -19
- package/dist/lib/bot/inbox-poll-backoff-flag.d.ts +3 -5
- package/dist/lib/bot/inbox-poll-backoff-flag.js +20 -12
- package/dist/lib/bot/inbox-wake.d.ts +18 -0
- package/dist/lib/bot/inbox-wake.js +125 -0
- package/dist/lib/bot/run.d.ts +8 -0
- package/dist/lib/bot/run.js +51 -7
- package/dist/lib/bot/runtime/messages-stream.js +31 -3
- package/dist/lib/core-utils/workflow-runner-schema.d.ts +7 -0
- package/dist/lib/core-utils/workflow-runner-schema.js +259 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +6 -3
- package/dist/lib/daemon/flag-monitor.d.ts +4 -1
- package/dist/lib/daemon/flag-monitor.js +12 -6
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +11 -5
- package/dist/lib/daemon/hq-child-bootstrap.mjs +47 -7
- package/dist/lib/daemon/kill-switch-flags.d.ts +5 -0
- package/dist/lib/daemon/kill-switch-flags.js +5 -0
- package/dist/lib/daemon/lanes-reconcile-loader.d.ts +7 -0
- package/dist/lib/daemon/lanes-reconcile-loader.js +24 -0
- package/dist/lib/daemon/lanes-reconcile-watcher.d.ts +17 -0
- package/dist/lib/daemon/lanes-reconcile-watcher.js +80 -0
- package/dist/lib/daemon/remote-control.d.ts +8 -5
- package/dist/lib/daemon/remote-control.js +8 -7
- package/dist/lib/daemon/run.d.ts +4 -1
- package/dist/lib/daemon/run.js +133 -20
- package/dist/lib/doctor/json-output.d.ts +15 -3
- package/dist/lib/doctor/json-output.js +13 -1
- package/dist/lib/lanes/config.d.ts +8 -4
- package/dist/lib/lanes/config.js +30 -36
- package/dist/lib/lanes/daemon-reconcile.d.ts +3 -0
- package/dist/lib/lanes/daemon-reconcile.js +8 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile-result.d.ts +10 -0
- package/dist/lib/lanes/reconcile-result.js +11 -0
- package/dist/lib/lanes/reconcile.js +3 -26
- package/dist/lib/lanes/throttle.js +5 -9
- package/dist/lib/lanes/types.d.ts +4 -5
- package/dist/lib/lanes/types.js +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.js +8 -1
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +2 -0
- package/dist/lib/mesh/story-status.d.ts +2 -2
- package/dist/lib/mesh/story-status.js +2 -1
- package/dist/utils/resident-package-preload.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.130] — 2026-10-14
|
|
6
|
+
- Add native TypeScript `hq core workflow run` orchestration with the bundled runner as its flag-off fallback.
|
|
7
|
+
|
|
8
|
+
- `hq lanes` no longer limits or pauses lanes by default. Providers have no
|
|
9
|
+
running or queued cap, admission no longer waits on CPU load or free memory,
|
|
10
|
+
and the `lead_reserve_per_host` Claude slot reserve is removed (the key is
|
|
11
|
+
ignored if present). Set `providers.<name>.max_running`, `max_queued`, or
|
|
12
|
+
`throttle.cpu_busy_pause` / `cpu_busy_resume` / `min_available_mem_mb` in
|
|
13
|
+
the lanes config file to opt back in.
|
|
14
|
+
|
|
15
|
+
## [5.345.129] — 2026-10-14
|
|
16
|
+
|
|
17
|
+
- Claude Remote Control account pools now stay connected across Claude CLI updates. The daemon still restarts a pool if its host exits, but no longer kills busy pool sessions after the two-hour update deadline.
|
|
18
|
+
|
|
19
|
+
- The resident daemon now preloads the lane reconcile module that its in-process lane watcher loads. Main CI had been failing the resident preload coverage test since the idle-wakeup change (#1920).
|
|
20
|
+
- Session-start health no longer files a stale personal sync-manifest warning (`sync.manifest.personal`) as an unrepaired problem after `hq doctor --fix`. `--fix` cannot upload personal manifests, so `hq doctor --json` and `--client-health-report` now omit that WARN the same way they omit the leftover personal-journal WARN. A FAIL on that row still shows, and plain `hq doctor` still prints the WARN.
|
|
21
|
+
- Local bots now recover on their own when a saved Claude conversation no longer
|
|
22
|
+
exists. Before, every message in that chat failed with
|
|
23
|
+
`error_during_execution` until the bot's session file was removed by hand.
|
|
24
|
+
- Fix mesh project ensure dropping PRD stories in the queued state. PRD files without a userStories array now fail before Board writes, and unsupported ledger states cannot produce a partial projection.
|
|
25
|
+
- Idle local bots now wake from Work Mesh inbox notifications with a 60-second recovery pull. The daemon watches lane and child-handoff files, scans for sync processes only when its unit is missing, and leaves post-sync reindexing to compatible hq-cloud 6.18.75 or later runners.
|
|
26
|
+
|
|
5
27
|
## [5.345.128] — 2026-10-14
|
|
6
28
|
- `hq auth refresh` now says which identity it ensured and where its token file is, for example `HQ session ensured (machine identity <name>; token file: <path>)` or `HQ session ensured (person session; token file: <path>)`. The identity is read after the refresh, so a fallback to the machine identity is reported as machine. Token values are never printed.
|
|
7
29
|
- `hq auth refresh --json` prints `{refreshed, tokenSource, tokenFile, reason?}` on stdout and exits 1 when no session was refreshed.
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Codex structured-output ("strict") schema adaptation for the workflow runner.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS
|
|
4
|
+
// `codex exec --output-schema <file>` forwards that file to the model provider
|
|
5
|
+
// as a STRICT structured-output schema (`text.format.schema`). Strict is a
|
|
6
|
+
// NARROWER dialect than the ordinary JSON Schema a workflow script writes, and
|
|
7
|
+
// the provider rejects the whole request — before the agent takes a single turn
|
|
8
|
+
// of work — when a node breaks one of its structural rules. Verified live
|
|
9
|
+
// against codex-cli 0.144.5 on 2026-08-19 (HTTP 400 `invalid_json_schema`):
|
|
10
|
+
//
|
|
11
|
+
// 1. every object node must carry `additionalProperties: false`
|
|
12
|
+
// In context=(), 'additionalProperties' is required to be supplied and
|
|
13
|
+
// to be false.
|
|
14
|
+
// 2. every object node's `required` must list EVERY key in `properties`
|
|
15
|
+
// In context=(), 'required' is required to be supplied and to be an
|
|
16
|
+
// array including every key in properties. Missing 'b'.
|
|
17
|
+
// 3. every schema node must declare a type
|
|
18
|
+
// In context=('properties', 'a'), schema must have a 'type' key.
|
|
19
|
+
//
|
|
20
|
+
// Rule 1 killed the first agent of every /orchestrate run on the codex engine;
|
|
21
|
+
// rule 2 is what the same run hits the moment rule 1 is satisfied. The stdout
|
|
22
|
+
// engines (grok, claude) have no schema flag — they are asked for the shape
|
|
23
|
+
// in-prompt and validated locally — so the dialect problem is codex-only, and
|
|
24
|
+
// so is this module.
|
|
25
|
+
//
|
|
26
|
+
// THE TRANSLATION
|
|
27
|
+
// Ordinary JSON Schema says "optional" by leaving a key out of `required`.
|
|
28
|
+
// Strict has no notion of optionality, so the faithful translation of an
|
|
29
|
+
// optional property is "required, but nullable": the key is always present and
|
|
30
|
+
// the model answers null when it has nothing to say. A plain typed node gets
|
|
31
|
+
// `null` added to its type; a composed node (`anyOf`/`oneOf`/`allOf`/`$ref`)
|
|
32
|
+
// gets a `{type: 'null'}` alternative instead, since a composed node carries no
|
|
33
|
+
// type of its own to widen. Both forms were confirmed accepted by the provider.
|
|
34
|
+
// `strictifySchemaForCodex()` makes that translation on the way out;
|
|
35
|
+
// `stripStrictNulls()` undoes it on the way back, deleting exactly the nulls
|
|
36
|
+
// the widening invited. A script therefore keeps declaring optionality the
|
|
37
|
+
// normal way, sees the same result shape on every engine (an optional field the
|
|
38
|
+
// model had nothing for is ABSENT, not null), and only the codex wire format
|
|
39
|
+
// changes.
|
|
40
|
+
//
|
|
41
|
+
// WHAT IT REFUSES TO TRANSLATE
|
|
42
|
+
// Two shapes have no strict equivalent, and both are rejected loudly here
|
|
43
|
+
// rather than quietly reinterpreted — a schema silently stripped of meaning
|
|
44
|
+
// returns data that looks complete and is not:
|
|
45
|
+
//
|
|
46
|
+
// - a free-form map (`additionalProperties: true`, or an additionalProperties
|
|
47
|
+
// sub-schema). Strict permits no dynamic keys at all; forcing `false` would
|
|
48
|
+
// drop exactly the keys the author asked for.
|
|
49
|
+
// - a `required` name with no matching property. Strict rejects the extra
|
|
50
|
+
// name, and dropping it would leave the runner's own validator demanding a
|
|
51
|
+
// key the model can no longer emit — a result that could never validate.
|
|
52
|
+
//
|
|
53
|
+
// Both throw before the agent is spawned, naming the offending path, so the
|
|
54
|
+
// author sees a fixable contract error instead of an opaque 400 or a silently
|
|
55
|
+
// truncated result.
|
|
56
|
+
//
|
|
57
|
+
// Both exported functions are pure — they never mutate their arguments —
|
|
58
|
+
// because the runner keeps using the script's own schema for the in-prompt copy
|
|
59
|
+
// and for local result validation, and workflow scripts routinely share one
|
|
60
|
+
// schema constant across several agent() calls.
|
|
61
|
+
|
|
62
|
+
function isPlainObject(value) {
|
|
63
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function typesOf(node) {
|
|
67
|
+
if (!isPlainObject(node) || node.type === undefined) return [];
|
|
68
|
+
return Array.isArray(node.type) ? node.type.slice() : [node.type];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// An object node is one that declares type "object" (possibly among others,
|
|
72
|
+
// e.g. ["object","null"]), or that declares no type but carries `properties` —
|
|
73
|
+
// the shorthand a hand-written schema often uses.
|
|
74
|
+
function isObjectNode(node) {
|
|
75
|
+
const types = typesOf(node);
|
|
76
|
+
if (types.length) return types.includes('object');
|
|
77
|
+
return isPlainObject(node.properties);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isComposed(node) {
|
|
81
|
+
return isPlainObject(node)
|
|
82
|
+
&& (Array.isArray(node.anyOf) || Array.isArray(node.oneOf)
|
|
83
|
+
|| Array.isArray(node.allOf) || typeof node.$ref === 'string');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sub-schema positions to recurse through. Keyed by how the value is shaped so
|
|
87
|
+
// a `properties` map named "items" (a legal property name) is never mistaken
|
|
88
|
+
// for the `items` keyword.
|
|
89
|
+
const SUBSCHEMA_MAPS = ['properties', '$defs', 'definitions'];
|
|
90
|
+
const SUBSCHEMA_LISTS = ['anyOf', 'oneOf', 'allOf', 'prefixItems'];
|
|
91
|
+
const SUBSCHEMA_SINGLES = ['items', 'not', 'if', 'then', 'else', 'contains', 'propertyNames'];
|
|
92
|
+
|
|
93
|
+
function schemaError(at, message) {
|
|
94
|
+
return new Error(`codex output schema at ${at}: ${message}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Optional -> "required but nullable". A typed node widens its own type; a
|
|
98
|
+
// composed node has no type to widen, so it gains a null ALTERNATIVE instead.
|
|
99
|
+
function widenToNullable(node, at) {
|
|
100
|
+
if (!isPlainObject(node)) {
|
|
101
|
+
// A boolean schema (`true`/`false`) carries no shape to widen. `true`
|
|
102
|
+
// already admits null; `false` admits nothing and is the author's business.
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const types = typesOf(node);
|
|
107
|
+
if (types.length) {
|
|
108
|
+
if (!types.includes('null')) node.type = [...types, 'null'];
|
|
109
|
+
// An enum must gain null too, or the widened type is unusable: no enum
|
|
110
|
+
// member could satisfy it.
|
|
111
|
+
if (Array.isArray(node.enum) && !node.enum.some((v) => v === null)) node.enum = [...node.enum, null];
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const key of ['anyOf', 'oneOf']) {
|
|
116
|
+
if (!Array.isArray(node[key]) || node[key].length === 0) continue;
|
|
117
|
+
if (!node[key].some((branch) => typesOf(branch).includes('null'))) node[key].push({ type: 'null' });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// `$ref` and `allOf` cannot take a sibling type (and `allOf` is an AND, so a
|
|
122
|
+
// null branch inside it would contradict the others) — wrap the whole node in
|
|
123
|
+
// an anyOf whose second branch is null.
|
|
124
|
+
if (typeof node.$ref === 'string' || Array.isArray(node.allOf)) {
|
|
125
|
+
const inner = { ...node };
|
|
126
|
+
for (const key of Object.keys(node)) delete node[key];
|
|
127
|
+
node.anyOf = [inner, { type: 'null' }];
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Object shorthand: `properties` with no declared type. Strict requires a
|
|
132
|
+
// type key anyway, so naming it here fixes two problems at once.
|
|
133
|
+
if (isPlainObject(node.properties)) {
|
|
134
|
+
node.type = ['object', 'null'];
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw schemaError(at,
|
|
139
|
+
'this property is optional (absent from `required`) but its schema declares '
|
|
140
|
+
+ 'neither a type nor a composition, so there is no way to express "no answer" '
|
|
141
|
+
+ 'in the strict dialect. Give it a type, or list it in `required`.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function strictifyInPlace(node, at) {
|
|
145
|
+
if (Array.isArray(node)) {
|
|
146
|
+
node.forEach((child, i) => strictifyInPlace(child, `${at}[${i}]`));
|
|
147
|
+
return node;
|
|
148
|
+
}
|
|
149
|
+
if (!isPlainObject(node)) return node;
|
|
150
|
+
|
|
151
|
+
for (const key of SUBSCHEMA_MAPS) {
|
|
152
|
+
if (!isPlainObject(node[key])) continue;
|
|
153
|
+
for (const [name, child] of Object.entries(node[key])) strictifyInPlace(child, `${at}.${key}.${name}`);
|
|
154
|
+
}
|
|
155
|
+
for (const key of SUBSCHEMA_LISTS) {
|
|
156
|
+
if (Array.isArray(node[key])) node[key].forEach((child, i) => strictifyInPlace(child, `${at}.${key}[${i}]`));
|
|
157
|
+
}
|
|
158
|
+
for (const key of SUBSCHEMA_SINGLES) {
|
|
159
|
+
if (node[key] !== undefined) strictifyInPlace(node[key], `${at}.${key}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!isObjectNode(node)) return node;
|
|
163
|
+
|
|
164
|
+
// A free-form map is the one thing strict cannot express. Say so instead of
|
|
165
|
+
// narrowing it away and returning an object missing its dynamic keys.
|
|
166
|
+
if (node.additionalProperties !== undefined && node.additionalProperties !== false) {
|
|
167
|
+
throw schemaError(at,
|
|
168
|
+
'strict structured output permits no dynamic keys, so `additionalProperties` '
|
|
169
|
+
+ `must be false (got ${JSON.stringify(node.additionalProperties)}). Model a map `
|
|
170
|
+
+ 'as an array of {key, value} entries, or run this agent on the grok or '
|
|
171
|
+
+ 'claude engine, which have no strict-schema flag.');
|
|
172
|
+
}
|
|
173
|
+
node.additionalProperties = false;
|
|
174
|
+
// Strict requires an explicit type; the object shorthand omits it.
|
|
175
|
+
if (typesOf(node).length === 0) node.type = 'object';
|
|
176
|
+
|
|
177
|
+
const props = isPlainObject(node.properties) ? node.properties : null;
|
|
178
|
+
if (!props) return node;
|
|
179
|
+
|
|
180
|
+
const keys = Object.keys(props);
|
|
181
|
+
const declared = Array.isArray(node.required) ? node.required : [];
|
|
182
|
+
const orphans = declared.filter((key) => !keys.includes(key));
|
|
183
|
+
if (orphans.length) {
|
|
184
|
+
throw schemaError(at,
|
|
185
|
+
`\`required\` lists ${orphans.map((k) => JSON.stringify(k)).join(', ')} with no matching `
|
|
186
|
+
+ 'property. Strict rejects a required name that is not in `properties`, and dropping it '
|
|
187
|
+
+ 'would leave the result validator demanding a key the model can no longer emit. '
|
|
188
|
+
+ 'Add the property, or remove the name from `required`.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Declared-required first (stable order), then the rest — each widened so the
|
|
192
|
+
// model can still answer "nothing here" for a property the script left optional.
|
|
193
|
+
const required = [];
|
|
194
|
+
for (const key of declared) if (!required.includes(key)) required.push(key);
|
|
195
|
+
for (const key of keys) {
|
|
196
|
+
if (required.includes(key)) continue;
|
|
197
|
+
widenToNullable(props[key], `${at}.properties.${key}`);
|
|
198
|
+
required.push(key);
|
|
199
|
+
}
|
|
200
|
+
node.required = required;
|
|
201
|
+
return node;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Returns a strict-dialect copy of `schema`, safe to write to --output-schema.
|
|
205
|
+
// Non-object inputs (a boolean schema, a stray string) pass through untouched.
|
|
206
|
+
// Throws on a schema whose meaning strict cannot carry — see the header.
|
|
207
|
+
export function strictifySchemaForCodex(schema) {
|
|
208
|
+
if (!isPlainObject(schema) && !Array.isArray(schema)) return schema;
|
|
209
|
+
// A JSON round-trip is a faithful clone here: the result is about to be
|
|
210
|
+
// JSON.stringify'd into the schema file, so anything it would drop could not
|
|
211
|
+
// have reached codex anyway.
|
|
212
|
+
return strictifyInPlace(JSON.parse(JSON.stringify(schema)), '<root>');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const MAX_REF_DEPTH = 16;
|
|
216
|
+
|
|
217
|
+
// Resolve a local `#/$defs/x` (or `#/definitions/x`) reference against the root
|
|
218
|
+
// schema. Anything else — a remote or pointer-nested ref — is left alone, which
|
|
219
|
+
// makes the caller treat it as "shape unknown" and leave the value untouched.
|
|
220
|
+
function resolveRef(node, root, depth = 0) {
|
|
221
|
+
if (!isPlainObject(node) || typeof node.$ref !== 'string' || depth >= MAX_REF_DEPTH) return node;
|
|
222
|
+
const match = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(node.$ref);
|
|
223
|
+
if (!match || !isPlainObject(root)) return node;
|
|
224
|
+
const bucket = root[match[1]];
|
|
225
|
+
const target = isPlainObject(bucket) ? bucket[match[2]] : undefined;
|
|
226
|
+
return isPlainObject(target) ? resolveRef(target, root, depth + 1) : node;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// A property may legitimately hold null when its own schema says so — only the
|
|
230
|
+
// nulls our widening invited get removed. Composition-aware: a union that has
|
|
231
|
+
// no null branch does not allow null either.
|
|
232
|
+
function allowsNull(node, root, depth = 0) {
|
|
233
|
+
const resolved = resolveRef(node, root, depth);
|
|
234
|
+
if (!isPlainObject(resolved)) return true;
|
|
235
|
+
const types = typesOf(resolved);
|
|
236
|
+
if (types.length) return types.includes('null');
|
|
237
|
+
for (const key of ['anyOf', 'oneOf']) {
|
|
238
|
+
if (Array.isArray(resolved[key])) {
|
|
239
|
+
return resolved[key].some((branch) => depth < MAX_REF_DEPTH && allowsNull(branch, root, depth + 1));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Object shorthand (`properties`, no declared type) is an object node, not a
|
|
243
|
+
// nullable one — and it is a shape this module widens, so the null it comes
|
|
244
|
+
// back with is ours to remove. Missing this made a live codex answer keep
|
|
245
|
+
// `"meta": null` for a property the script had declared as an object.
|
|
246
|
+
if (isPlainObject(resolved.properties)) return false;
|
|
247
|
+
return true; // nothing declared — do not touch the value
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// The schema to descend into for a value. A `$ref` resolves; a union resolves
|
|
251
|
+
// only when exactly one branch is non-null (the "optional X" shape, including
|
|
252
|
+
// the one our own widening produces). A genuinely ambiguous union returns null
|
|
253
|
+
// and the subtree is left alone rather than stripped on a guess.
|
|
254
|
+
function effectiveSchema(node, root, depth = 0) {
|
|
255
|
+
const resolved = resolveRef(node, root, depth);
|
|
256
|
+
if (!isPlainObject(resolved)) return null;
|
|
257
|
+
for (const key of ['anyOf', 'oneOf']) {
|
|
258
|
+
if (!Array.isArray(resolved[key])) continue;
|
|
259
|
+
const branches = resolved[key]
|
|
260
|
+
.map((branch) => resolveRef(branch, root, depth + 1))
|
|
261
|
+
.filter((branch) => isPlainObject(branch))
|
|
262
|
+
.filter((branch) => !(typesOf(branch).length === 1 && typesOf(branch)[0] === 'null'));
|
|
263
|
+
if (branches.length !== 1 || depth >= MAX_REF_DEPTH) return null;
|
|
264
|
+
return effectiveSchema(branches[0], root, depth + 1);
|
|
265
|
+
}
|
|
266
|
+
return resolved;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Mirrors the runner's own validator (properties + items), plus the `$ref` and
|
|
270
|
+
// single-alternative union forms this module can widen — so a value that
|
|
271
|
+
// survives this walk is shaped the way that validator will read it.
|
|
272
|
+
function stripInPlace(value, schema, root, depth = 0) {
|
|
273
|
+
const effective = effectiveSchema(schema, root, depth);
|
|
274
|
+
if (!isPlainObject(effective)) return value;
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
if (effective.items !== undefined) {
|
|
277
|
+
for (const item of value) stripInPlace(item, effective.items, root, depth + 1);
|
|
278
|
+
}
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
if (!isPlainObject(value)) return value;
|
|
282
|
+
const props = isPlainObject(effective.properties) ? effective.properties : null;
|
|
283
|
+
if (!props) return value;
|
|
284
|
+
const required = Array.isArray(effective.required) ? effective.required : [];
|
|
285
|
+
for (const [key, sub] of Object.entries(props)) {
|
|
286
|
+
if (!(key in value)) continue;
|
|
287
|
+
if (value[key] === null && !required.includes(key) && !allowsNull(sub, root)) {
|
|
288
|
+
delete value[key];
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
stripInPlace(value[key], sub, root, depth + 1);
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Undoes the optional -> nullable widening on a codex result: a null answer for
|
|
297
|
+
// a property the ORIGINAL schema left optional (and did not itself declare
|
|
298
|
+
// nullable) becomes an absent key, which is what the script asked for and what
|
|
299
|
+
// every other engine returns.
|
|
300
|
+
export function stripStrictNulls(value, schema) {
|
|
301
|
+
if (!isPlainObject(schema)) return value;
|
|
302
|
+
if (value === null || typeof value !== 'object') return value;
|
|
303
|
+
return stripInPlace(JSON.parse(JSON.stringify(value)), schema, schema);
|
|
304
|
+
}
|