jev-layer 0.1.0 → 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 (83) hide show
  1. package/CHANGELOG.md +45 -34
  2. package/CONTRIBUTING.md +146 -146
  3. package/LICENSE +21 -21
  4. package/README.md +123 -126
  5. package/README.ru.md +138 -117
  6. package/README.zh-CN.md +137 -117
  7. package/RELEASE.md +53 -53
  8. package/SECURITY.md +56 -56
  9. package/bin/jev.mjs +213 -213
  10. package/config/codex.mcp.toml +8 -8
  11. package/config/generic-mcp.json +13 -13
  12. package/config/hermes.mcp.yaml +9 -9
  13. package/config/jev.example.json +22 -22
  14. package/config/omp.mcp.json +14 -14
  15. package/config/providers.env.example +13 -13
  16. package/docs/AGENT-IMPLEMENTATION.md +250 -0
  17. package/docs/COMPATIBILITY.md +15 -15
  18. package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +177 -0
  19. package/docs/PROVIDERS.md +67 -0
  20. package/docs/SCHEMA-VERSIONING.md +96 -94
  21. package/docs/architecture.svg +77 -0
  22. package/docs/jev-layer-banner.svg +38 -0
  23. package/examples/capabilities.json +39 -39
  24. package/examples/route-request.json +28 -28
  25. package/integrations/codex/.codex-plugin/plugin.json +19 -19
  26. package/integrations/codex/.mcp.json +11 -11
  27. package/integrations/codex/AGENTS.md +1 -1
  28. package/integrations/codex/run-mcp.mjs +9 -9
  29. package/integrations/codex/skills/jev-route/SKILL.md +17 -17
  30. package/integrations/hermes/__init__.py +150 -51
  31. package/integrations/hermes/plugin.yaml +10 -5
  32. package/integrations/hermes/schemas.py +89 -12
  33. package/integrations/omp/extension.js +105 -105
  34. package/integrations/template/README.md +10 -10
  35. package/integrations/template/adapter.mjs +87 -87
  36. package/package.json +60 -59
  37. package/scripts/benchmark.mjs +35 -35
  38. package/scripts/browser-benchmark.mjs +92 -92
  39. package/scripts/browser-e2e.mjs +117 -117
  40. package/scripts/capability-e2e.mjs +31 -31
  41. package/scripts/clean-install-smoke.mjs +162 -162
  42. package/scripts/codex-mcp-smoke.mjs +119 -119
  43. package/scripts/context-filter-e2e.mjs +45 -45
  44. package/scripts/fail-open-smoke.mjs +120 -120
  45. package/scripts/feature-flags-smoke.mjs +46 -46
  46. package/scripts/mcp-receipt-smoke.mjs +113 -98
  47. package/scripts/model-route-report.mjs +15 -0
  48. package/scripts/openrouter-choice.mjs +59 -59
  49. package/scripts/replay-eval.mjs +124 -124
  50. package/scripts/smoke.mjs +34 -34
  51. package/scripts/supervision-e2e.mjs +138 -109
  52. package/src/browser.mjs +571 -569
  53. package/src/cli.mjs +33 -33
  54. package/src/config.mjs +85 -75
  55. package/src/context-filter.mjs +56 -56
  56. package/src/contract.mjs +72 -72
  57. package/src/discovery.mjs +65 -65
  58. package/src/hermes-adapter.mjs +123 -0
  59. package/src/mcp-server.mjs +267 -210
  60. package/src/model-route-metrics.mjs +89 -0
  61. package/src/model-routing.mjs +87 -0
  62. package/src/providers/demo.mjs +52 -52
  63. package/src/providers/index.mjs +12 -0
  64. package/src/providers/typesafe.mjs +126 -126
  65. package/src/receipts.mjs +227 -226
  66. package/src/registry.mjs +109 -109
  67. package/src/relevance-filter.mjs +99 -99
  68. package/src/route.mjs +219 -221
  69. package/src/shadow-compaction.mjs +170 -0
  70. package/src/supervision.mjs +268 -244
  71. package/test/browser.test.mjs +199 -199
  72. package/test/capability.test.mjs +54 -54
  73. package/test/context-filter.test.mjs +65 -65
  74. package/test/hermes-adapter.test.mjs +62 -0
  75. package/test/model-route-metrics.test.mjs +61 -0
  76. package/test/model-routing.test.mjs +59 -0
  77. package/test/openrouter-provider.test.mjs +55 -55
  78. package/test/provider-transports.test.mjs +205 -0
  79. package/test/providers.test.mjs +126 -0
  80. package/test/receipts.test.mjs +71 -71
  81. package/test/route.test.mjs +74 -74
  82. package/test/shadow-compaction.test.mjs +67 -0
  83. package/test/supervision.test.mjs +117 -99
package/src/registry.mjs CHANGED
@@ -1,109 +1,109 @@
1
- import { CAPABILITY_KINDS, RISK_ORDER } from "./contract.mjs";
2
-
3
- const KIND_ALIASES = Object.freeze({ mcp_tool: "mcp" });
4
-
5
- export const DEFAULT_POLICY = Object.freeze({
6
- min_confidence: 0.62,
7
- max_risk: "high",
8
- confirmation_risk_levels: ["medium", "high"],
9
- });
10
-
11
- export function normalizeCapability(raw, index = 0) {
12
- if (!raw || typeof raw !== "object") throw new TypeError(`capabilities[${index}] must be an object`);
13
- if (typeof raw.id !== "string" || raw.id.trim() === "") throw new TypeError(`capabilities[${index}].id is required`);
14
- if (typeof raw.name !== "string" || raw.name.trim() === "") throw new TypeError(`capabilities[${index}].name is required`);
15
- if (typeof raw.description !== "string" || raw.description.trim() === "") throw new TypeError(`capabilities[${index}].description is required`);
16
-
17
- const rawKind = raw.kind ?? raw.type ?? raw.execution?.mode ?? "tool";
18
- const kind = KIND_ALIASES[rawKind] ?? rawKind;
19
- if (!CAPABILITY_KINDS.has(kind)) throw new TypeError(`capabilities[${index}].kind is unsupported: ${rawKind}`);
20
-
21
- const risk = raw.risk?.level ?? raw.risk ?? "medium";
22
- if (!(risk in RISK_ORDER)) throw new TypeError(`capabilities[${index}].risk is unsupported: ${risk}`);
23
-
24
- return {
25
- id: raw.id.trim(),
26
- kind,
27
- name: raw.name.trim(),
28
- description: raw.description.trim(),
29
- permissions: Array.isArray(raw.permissions) ? raw.permissions.filter((value) => typeof value === "string") : [],
30
- risk,
31
- available: raw.available !== false && raw.availability?.available !== false,
32
- availability_reason: raw.availability?.reason ?? null,
33
- source: typeof raw.source === "string" ? raw.source : null,
34
- verified: raw.verified === true,
35
- metadata: normalizeMetadata(raw.metadata ?? raw.discovery),
36
- execution: {
37
- mode: "host",
38
- target: typeof raw.execution?.target === "string" ? raw.execution.target : undefined,
39
- },
40
- requires_confirmation: Boolean(raw.policy?.requires_confirmation) || risk !== "low",
41
- };
42
- }
43
-
44
- export function normalizeCapabilities(rawCapabilities) {
45
- const seen = new Set();
46
- return rawCapabilities.map((raw, index) => {
47
- const capability = normalizeCapability(raw, index);
48
- if (seen.has(capability.id)) throw new TypeError(`duplicate capability id: ${capability.id}`);
49
- seen.add(capability.id);
50
- return capability;
51
- });
52
- }
53
-
54
- export function filterCapabilities(capabilities, request, policy = {}) {
55
- const effective = { ...DEFAULT_POLICY, ...policy };
56
- const maxRisk = effective.max_risk ?? "high";
57
- const actorPermissions = Array.isArray(request.actor_permissions) ? new Set(request.actor_permissions) : null;
58
- const assessments = capabilities.map((capability) => {
59
- let reason = null;
60
- if (!capability.available) reason = capability.availability_reason || "capability unavailable";
61
- else if (RISK_ORDER[capability.risk] > RISK_ORDER[maxRisk]) reason = `risk ${capability.risk} exceeds policy maximum ${maxRisk}`;
62
- else if (actorPermissions && capability.permissions.some((permission) => !actorPermissions.has(permission))) reason = "actor lacks required capability permissions";
63
- return { capability, filtered: Boolean(reason), filter_reason: reason };
64
- });
65
- return {
66
- policy: effective,
67
- assessments,
68
- eligible: assessments.filter((assessment) => !assessment.filtered).map((assessment) => assessment.capability),
69
- };
70
- }
71
- export function decisionCandidates(assessments, probabilities = {}, confidence = null) {
72
- return assessments.map(({ capability, filtered, filter_reason }) => ({
73
- id: capability.id,
74
- kind: capability.kind,
75
- name: capability.name,
76
- risk: capability.risk,
77
- available: capability.available,
78
- filtered,
79
- filter_reason,
80
- probability: typeof probabilities[capability.id] === "number" ? probabilities[capability.id] : null,
81
- confidence: confidence ?? null,
82
- requires_confirmation: capability.requires_confirmation,
83
- source: capability.source,
84
- verified: capability.verified,
85
- metadata: capability.metadata,
86
- }));
87
- }
88
-
89
- export function deterministicCandidate(capabilities, policy = {}) {
90
- let eligible = capabilities;
91
- const requestedId = typeof policy.deterministic_capability_id === "string" ? policy.deterministic_capability_id : null;
92
- if (requestedId) eligible = eligible.filter((capability) => capability.id === requestedId);
93
- if (typeof policy.prefer_kind === "string") eligible = eligible.filter((capability) => capability.kind === policy.prefer_kind);
94
- if (typeof policy.prefer_source === "string") eligible = eligible.filter((capability) => capability.source === policy.prefer_source);
95
- if (policy.prefer_verified === true) eligible = eligible.filter((capability) => capability.verified);
96
- if (eligible.length !== 1) return null;
97
- return {
98
- capability: eligible[0],
99
- reason: requestedId ? "explicit capability policy" : "one capability remains after deterministic policy",
100
- };
101
- }
102
-
103
- function normalizeMetadata(value) {
104
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
105
- const allowed = ["skill", "mcp", "cli", "dsh", "subagent", "model", "manifest", "source_ref"];
106
- return Object.fromEntries(allowed
107
- .filter((key) => typeof value[key] === "string" || typeof value[key] === "boolean")
108
- .map((key) => [key, value[key]]));
109
- }
1
+ import { CAPABILITY_KINDS, RISK_ORDER } from "./contract.mjs";
2
+
3
+ const KIND_ALIASES = Object.freeze({ mcp_tool: "mcp" });
4
+
5
+ export const DEFAULT_POLICY = Object.freeze({
6
+ min_confidence: 0.62,
7
+ max_risk: "high",
8
+ confirmation_risk_levels: ["medium", "high"],
9
+ });
10
+
11
+ export function normalizeCapability(raw, index = 0) {
12
+ if (!raw || typeof raw !== "object") throw new TypeError(`capabilities[${index}] must be an object`);
13
+ if (typeof raw.id !== "string" || raw.id.trim() === "") throw new TypeError(`capabilities[${index}].id is required`);
14
+ if (typeof raw.name !== "string" || raw.name.trim() === "") throw new TypeError(`capabilities[${index}].name is required`);
15
+ if (typeof raw.description !== "string" || raw.description.trim() === "") throw new TypeError(`capabilities[${index}].description is required`);
16
+
17
+ const rawKind = raw.kind ?? raw.type ?? raw.execution?.mode ?? "tool";
18
+ const kind = KIND_ALIASES[rawKind] ?? rawKind;
19
+ if (!CAPABILITY_KINDS.has(kind)) throw new TypeError(`capabilities[${index}].kind is unsupported: ${rawKind}`);
20
+
21
+ const risk = raw.risk?.level ?? raw.risk ?? "medium";
22
+ if (!(risk in RISK_ORDER)) throw new TypeError(`capabilities[${index}].risk is unsupported: ${risk}`);
23
+
24
+ return {
25
+ id: raw.id.trim(),
26
+ kind,
27
+ name: raw.name.trim(),
28
+ description: raw.description.trim(),
29
+ permissions: Array.isArray(raw.permissions) ? raw.permissions.filter((value) => typeof value === "string") : [],
30
+ risk,
31
+ available: raw.available !== false && raw.availability?.available !== false,
32
+ availability_reason: raw.availability?.reason ?? null,
33
+ source: typeof raw.source === "string" ? raw.source : null,
34
+ verified: raw.verified === true,
35
+ metadata: normalizeMetadata(raw.metadata ?? raw.discovery),
36
+ execution: {
37
+ mode: "host",
38
+ target: typeof raw.execution?.target === "string" ? raw.execution.target : undefined,
39
+ },
40
+ requires_confirmation: Boolean(raw.policy?.requires_confirmation) || risk !== "low",
41
+ };
42
+ }
43
+
44
+ export function normalizeCapabilities(rawCapabilities) {
45
+ const seen = new Set();
46
+ return rawCapabilities.map((raw, index) => {
47
+ const capability = normalizeCapability(raw, index);
48
+ if (seen.has(capability.id)) throw new TypeError(`duplicate capability id: ${capability.id}`);
49
+ seen.add(capability.id);
50
+ return capability;
51
+ });
52
+ }
53
+
54
+ export function filterCapabilities(capabilities, request, policy = {}) {
55
+ const effective = { ...DEFAULT_POLICY, ...policy };
56
+ const maxRisk = effective.max_risk ?? "high";
57
+ const actorPermissions = Array.isArray(request.actor_permissions) ? new Set(request.actor_permissions) : null;
58
+ const assessments = capabilities.map((capability) => {
59
+ let reason = null;
60
+ if (!capability.available) reason = capability.availability_reason || "capability unavailable";
61
+ else if (RISK_ORDER[capability.risk] > RISK_ORDER[maxRisk]) reason = `risk ${capability.risk} exceeds policy maximum ${maxRisk}`;
62
+ else if (actorPermissions && capability.permissions.some((permission) => !actorPermissions.has(permission))) reason = "actor lacks required capability permissions";
63
+ return { capability, filtered: Boolean(reason), filter_reason: reason };
64
+ });
65
+ return {
66
+ policy: effective,
67
+ assessments,
68
+ eligible: assessments.filter((assessment) => !assessment.filtered).map((assessment) => assessment.capability),
69
+ };
70
+ }
71
+ export function decisionCandidates(assessments, probabilities = {}, confidence = null) {
72
+ return assessments.map(({ capability, filtered, filter_reason }) => ({
73
+ id: capability.id,
74
+ kind: capability.kind,
75
+ name: capability.name,
76
+ risk: capability.risk,
77
+ available: capability.available,
78
+ filtered,
79
+ filter_reason,
80
+ probability: typeof probabilities[capability.id] === "number" ? probabilities[capability.id] : null,
81
+ confidence: confidence ?? null,
82
+ requires_confirmation: capability.requires_confirmation,
83
+ source: capability.source,
84
+ verified: capability.verified,
85
+ metadata: capability.metadata,
86
+ }));
87
+ }
88
+
89
+ export function deterministicCandidate(capabilities, policy = {}) {
90
+ let eligible = capabilities;
91
+ const requestedId = typeof policy.deterministic_capability_id === "string" ? policy.deterministic_capability_id : null;
92
+ if (requestedId) eligible = eligible.filter((capability) => capability.id === requestedId);
93
+ if (typeof policy.prefer_kind === "string") eligible = eligible.filter((capability) => capability.kind === policy.prefer_kind);
94
+ if (typeof policy.prefer_source === "string") eligible = eligible.filter((capability) => capability.source === policy.prefer_source);
95
+ if (policy.prefer_verified === true) eligible = eligible.filter((capability) => capability.verified);
96
+ if (eligible.length !== 1) return null;
97
+ return {
98
+ capability: eligible[0],
99
+ reason: requestedId ? "explicit capability policy" : "one capability remains after deterministic policy",
100
+ };
101
+ }
102
+
103
+ function normalizeMetadata(value) {
104
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
105
+ const allowed = ["skill", "mcp", "cli", "dsh", "subagent", "model", "manifest", "source_ref"];
106
+ return Object.fromEntries(allowed
107
+ .filter((key) => typeof value[key] === "string" || typeof value[key] === "boolean")
108
+ .map((key) => [key, value[key]]));
109
+ }
@@ -1,99 +1,99 @@
1
- const MODES = new Set(["shadow", "conservative"]);
2
- const SIGNALS = [
3
- /(?:^|[\s"'`])(?:\/[\w.\-~]+|~\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/u,
4
- /\b(?:error|exception|traceback|failed|failure|exit\s+status|stderr)\b/i,
5
- /`[^`\n]+`/u,
6
- /(?:^|\n)\s*(?:[$#])\s*[A-Za-z0-9_.-]+/u,
7
- /\b(?:must|required|requirement|acceptance|should|need\s+to)\b/i,
8
- ];
9
- const LIST_KEYS = new Set(["messages", "events", "logs", "tool_results", "history", "transcript"]);
10
-
11
- export function normalizeRelevanceMode(mode) {
12
- if (mode === true) return "shadow";
13
- if (typeof mode !== "string") return null;
14
- const normalized = mode.trim().toLowerCase();
15
- return MODES.has(normalized) ? normalized : null;
16
- }
17
-
18
- export function filterContext(context, { mode = "shadow", recent = 8 } = {}) {
19
- const normalizedMode = normalizeRelevanceMode(mode);
20
- if (!normalizedMode) return { context, report: disabledReport() };
21
- const source = context && typeof context === "object" ? context : {};
22
- const report = {
23
- mode: normalizedMode,
24
- changed: false,
25
- considered: 0,
26
- kept: 0,
27
- dropped: 0,
28
- pinned: 0,
29
- pinned_reasons: [],
30
- };
31
- if (normalizedMode === "shadow") {
32
- inspectValue(source, report);
33
- return { context: source, report };
34
- }
35
-
36
- const output = Array.isArray(source) ? source.slice() : { ...source };
37
- for (const [key, value] of Object.entries(source)) {
38
- if (!LIST_KEYS.has(key) || !Array.isArray(value)) continue;
39
- const filtered = filterList(value, normalizedMode, recent, report);
40
- output[key] = filtered;
41
- }
42
- if (report.dropped > 0) {
43
- report.changed = true;
44
- output._jev_relevance = {
45
- mode: normalizedMode,
46
- dropped: report.dropped,
47
- pinned: report.pinned,
48
- };
49
- }
50
- return { context: output, report };
51
- }
52
-
53
- export function isPinnedEvidence(value) {
54
- const text = typeof value === "string" ? value : JSON.stringify(value ?? "");
55
- const reasons = [];
56
- if (SIGNALS[0].test(text)) reasons.push("path");
57
- if (SIGNALS[1].test(text)) reasons.push("error");
58
- if (SIGNALS[2].test(text) || SIGNALS[3].test(text)) reasons.push("command");
59
- if (SIGNALS[4].test(text)) reasons.push("requirement");
60
- return { pinned: reasons.length > 0, reasons };
61
- }
62
-
63
- function filterList(items, mode, recent, report) {
64
- const keepFrom = Math.max(0, items.length - Math.max(0, recent));
65
- return items.filter((item, index) => {
66
- report.considered += 1;
67
- const evidence = isPinnedEvidence(item);
68
- const keep = evidence.pinned || index >= keepFrom || mode === "conservative" && index === items.length - 1;
69
- if (evidence.pinned) {
70
- report.pinned += 1;
71
- report.pinned_reasons.push(...evidence.reasons);
72
- }
73
- if (keep) report.kept += 1;
74
- else report.dropped += 1;
75
- return keep;
76
- });
77
- }
78
-
79
- function inspectValue(value, report) {
80
- if (Array.isArray(value)) {
81
- for (const item of value) {
82
- report.considered += 1;
83
- const evidence = isPinnedEvidence(item);
84
- if (evidence.pinned) {
85
- report.pinned += 1;
86
- report.pinned_reasons.push(...evidence.reasons);
87
- }
88
- inspectValue(item, report);
89
- }
90
- return;
91
- }
92
- if (value && typeof value === "object") {
93
- for (const child of Object.values(value)) inspectValue(child, report);
94
- }
95
- }
96
-
97
- function disabledReport() {
98
- return { mode: null, changed: false, considered: 0, kept: 0, dropped: 0, pinned: 0, pinned_reasons: [] };
99
- }
1
+ const MODES = new Set(["shadow", "conservative"]);
2
+ const SIGNALS = [
3
+ /(?:^|[\s"'`])(?:\/[\w.\-~]+|~\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/u,
4
+ /\b(?:error|exception|traceback|failed|failure|exit\s+status|stderr)\b/i,
5
+ /`[^`\n]+`/u,
6
+ /(?:^|\n)\s*(?:[$#])\s*[A-Za-z0-9_.-]+/u,
7
+ /\b(?:must|required|requirement|acceptance|should|need\s+to)\b/i,
8
+ ];
9
+ const LIST_KEYS = new Set(["messages", "events", "logs", "tool_results", "history", "transcript"]);
10
+
11
+ export function normalizeRelevanceMode(mode) {
12
+ if (mode === true) return "shadow";
13
+ if (typeof mode !== "string") return null;
14
+ const normalized = mode.trim().toLowerCase();
15
+ return MODES.has(normalized) ? normalized : null;
16
+ }
17
+
18
+ export function filterContext(context, { mode = "shadow", recent = 8 } = {}) {
19
+ const normalizedMode = normalizeRelevanceMode(mode);
20
+ if (!normalizedMode) return { context, report: disabledReport() };
21
+ const source = context && typeof context === "object" ? context : {};
22
+ const report = {
23
+ mode: normalizedMode,
24
+ changed: false,
25
+ considered: 0,
26
+ kept: 0,
27
+ dropped: 0,
28
+ pinned: 0,
29
+ pinned_reasons: [],
30
+ };
31
+ if (normalizedMode === "shadow") {
32
+ inspectValue(source, report);
33
+ return { context: source, report };
34
+ }
35
+
36
+ const output = Array.isArray(source) ? source.slice() : { ...source };
37
+ for (const [key, value] of Object.entries(source)) {
38
+ if (!LIST_KEYS.has(key) || !Array.isArray(value)) continue;
39
+ const filtered = filterList(value, normalizedMode, recent, report);
40
+ output[key] = filtered;
41
+ }
42
+ if (report.dropped > 0) {
43
+ report.changed = true;
44
+ output._jev_relevance = {
45
+ mode: normalizedMode,
46
+ dropped: report.dropped,
47
+ pinned: report.pinned,
48
+ };
49
+ }
50
+ return { context: output, report };
51
+ }
52
+
53
+ export function isPinnedEvidence(value) {
54
+ const text = typeof value === "string" ? value : JSON.stringify(value ?? "");
55
+ const reasons = [];
56
+ if (SIGNALS[0].test(text)) reasons.push("path");
57
+ if (SIGNALS[1].test(text)) reasons.push("error");
58
+ if (SIGNALS[2].test(text) || SIGNALS[3].test(text)) reasons.push("command");
59
+ if (SIGNALS[4].test(text)) reasons.push("requirement");
60
+ return { pinned: reasons.length > 0, reasons };
61
+ }
62
+
63
+ function filterList(items, mode, recent, report) {
64
+ const keepFrom = Math.max(0, items.length - Math.max(0, recent));
65
+ return items.filter((item, index) => {
66
+ report.considered += 1;
67
+ const evidence = isPinnedEvidence(item);
68
+ const keep = evidence.pinned || index >= keepFrom || mode === "conservative" && index === items.length - 1;
69
+ if (evidence.pinned) {
70
+ report.pinned += 1;
71
+ report.pinned_reasons.push(...evidence.reasons);
72
+ }
73
+ if (keep) report.kept += 1;
74
+ else report.dropped += 1;
75
+ return keep;
76
+ });
77
+ }
78
+
79
+ function inspectValue(value, report) {
80
+ if (Array.isArray(value)) {
81
+ for (const item of value) {
82
+ report.considered += 1;
83
+ const evidence = isPinnedEvidence(item);
84
+ if (evidence.pinned) {
85
+ report.pinned += 1;
86
+ report.pinned_reasons.push(...evidence.reasons);
87
+ }
88
+ inspectValue(item, report);
89
+ }
90
+ return;
91
+ }
92
+ if (value && typeof value === "object") {
93
+ for (const child of Object.values(value)) inspectValue(child, report);
94
+ }
95
+ }
96
+
97
+ function disabledReport() {
98
+ return { mode: null, changed: false, considered: 0, kept: 0, dropped: 0, pinned: 0, pinned_reasons: [] };
99
+ }