@aiwg/cli 2026.9.4 → 2026.9.5
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/THIRD_PARTY_NOTICES.md +16 -0
- package/agentic/code/providers/capability-matrix.yaml +45 -3
- package/agentic/code/providers/deepseek-harness/README.md +8 -0
- package/agentic/code/providers/deepseek-harness/aiwg.cordis.patch.yml +10 -0
- package/agentic/code/providers/model-capabilities.v1.json +11 -0
- package/agentic/code/providers/model-catalog.v1.json +8 -0
- package/bin/aiwg.mjs +1 -0
- package/dist/src/api/index.d.ts +14 -0
- package/dist/src/api/index.js +14 -0
- package/dist/src/artifacts/corpus-tools/source-types.js +1 -0
- package/dist/src/artifacts/index-files.js +17 -2
- package/dist/src/artifacts/repair.js +55 -6
- package/dist/src/catalog/cli.js +21 -7
- package/dist/src/catalog/cli.mjs +22 -7
- package/dist/src/cli/agent-spawn.js +10 -1
- package/dist/src/cli/handlers/artifacts.js +22 -3
- package/dist/src/cli/handlers/help.js +3 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/models.js +2 -2
- package/dist/src/cli/handlers/output-mode.js +1 -1
- package/dist/src/cli/handlers/runtime-info.js +1 -1
- package/dist/src/cli/handlers/sessions.js +55 -15
- package/dist/src/cli/handlers/steward.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +5 -0
- package/dist/src/cli/handlers/writer-profile.js +110 -0
- package/dist/src/cli/handlers/writing.js +122 -0
- package/dist/src/cli/router.js +5 -1
- package/dist/src/config/project-artifacts-runtime.mjs +33 -1
- package/dist/src/config/project-artifacts.js +1 -1
- package/dist/src/dataset/fortemi-dataset-execution.d.ts +23 -0
- package/dist/src/dataset/fortemi-dataset-execution.js +158 -0
- package/dist/src/dataset/fortemi-live-qualification.d.ts +4 -2
- package/dist/src/dataset/fortemi-live-qualification.js +20 -21
- package/dist/src/dataset/fortemi-run-receipt.d.ts +44 -0
- package/dist/src/dataset/fortemi-run-receipt.js +74 -0
- package/dist/src/dataset/index.d.ts +2 -0
- package/dist/src/dataset/index.js +2 -0
- package/dist/src/extensions/commands/definitions.js +24 -0
- package/dist/src/extensions/manifest.js +3 -0
- package/dist/src/mcp/server.mjs +2 -0
- package/dist/src/mcp/tools/writer-profiles.mjs +40 -0
- package/dist/src/models/model-capabilities.v1.json +11 -0
- package/dist/src/models/model-catalog.v1.json +8 -0
- package/dist/src/models/provider-policy.js +1 -1
- package/dist/src/network-analysis/analyzer.js +667 -0
- package/dist/src/network-analysis/citations.js +107 -0
- package/dist/src/network-analysis/forensics.js +132 -0
- package/dist/src/network-analysis/governance.js +216 -0
- package/dist/src/network-analysis/index.js +10 -0
- package/dist/src/network-analysis/probe.js +405 -0
- package/dist/src/network-analysis/recipes.js +88 -0
- package/dist/src/network-analysis/research.js +181 -0
- package/dist/src/network-analysis/termshark.js +252 -0
- package/dist/src/network-analysis/verification.js +171 -0
- package/dist/src/output-modes/registry.js +37 -6
- package/dist/src/output-modes/runtime.js +164 -28
- package/dist/src/providers/capability-matrix.yaml +45 -3
- package/dist/src/providers/provider-definitions.js +49 -0
- package/dist/src/providers/provider-inventory.js +1 -0
- package/dist/src/providers/transformation-receipt.js +3 -2
- package/dist/src/sessions/adapters/deepseek-harness.js +178 -0
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/contracts.js +2 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/workspace-discovery.js +5 -1
- package/dist/src/skills/deployer.js +6 -6
- package/dist/src/smiths/context-pipeline/workspace-context.js +7 -0
- package/dist/src/writing/channel-packs.js +13 -0
- package/dist/src/writing/contextual-diagnostics.js +142 -0
- package/dist/src/writing/example-generator.js +7 -6
- package/dist/src/writing/exemplar-selection.js +186 -0
- package/dist/src/writing/fidelity.js +61 -0
- package/dist/src/writing/validation-engine.js +32 -15
- package/dist/src/writing/voice-evaluation.js +301 -0
- package/dist/src/writing/voice-revision.js +201 -0
- package/dist/src/writing/writer-migration.js +216 -0
- package/dist/src/writing/writer-profile-legacy.js +145 -0
- package/dist/src/writing/writer-profile-store.js +117 -0
- package/dist/src/writing/writer-profile.js +222 -0
- package/dist/src/writing/writing-brief.js +166 -0
- package/dist/src/writing/writing-channels.js +63 -0
- package/dist/src/writing/writing-consumer.js +39 -0
- package/dist/src/writing/writing-receipt.js +266 -0
- package/package.json +1 -1
- package/schemas/dataset/fortemi-live-qualification-receipt.v2.schema.json +196 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/authority.json +12 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json +819 -0
- package/tools/agents/deploy-agents.mjs +7 -3
- package/tools/agents/providers/antigravity.mjs +1 -1
- package/tools/agents/providers/base.mjs +3 -2
- package/tools/agents/providers/deepseek-harness.mjs +66 -0
- package/tools/agents/providers/hermes.mjs +1 -1
- package/tools/agents/providers/openhuman.mjs +2 -2
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
+
import { access, constants, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
const execFile = promisify(execFileCallback);
|
|
7
|
+
export const NETWORK_ANALYSIS_PROBE_SCHEMA = 'aiwg.network-analysis.probe.v1';
|
|
8
|
+
const TOOL_SPECS = {
|
|
9
|
+
tshark: {
|
|
10
|
+
name: 'tshark',
|
|
11
|
+
required: true,
|
|
12
|
+
versionArgs: ['-v'],
|
|
13
|
+
capabilityCommands: [['-h'], ['-G', 'fields'], ['-z', 'help'], ['-F']],
|
|
14
|
+
},
|
|
15
|
+
capinfos: {
|
|
16
|
+
name: 'capinfos',
|
|
17
|
+
required: true,
|
|
18
|
+
versionArgs: ['-v'],
|
|
19
|
+
capabilityCommands: [['-h']],
|
|
20
|
+
},
|
|
21
|
+
termshark: {
|
|
22
|
+
name: 'termshark',
|
|
23
|
+
required: false,
|
|
24
|
+
versionArgs: ['--version'],
|
|
25
|
+
capabilityCommands: [['--help']],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
export const CURRENT_NETWORK_ANALYSIS_VERSION_POLICY = {
|
|
29
|
+
verifiedAt: '2026-09-05',
|
|
30
|
+
wiresharkStable: '4.6.8',
|
|
31
|
+
wiresharkOldStable: '4.4.18',
|
|
32
|
+
maintainedBranches: ['4.6', '4.4'],
|
|
33
|
+
minimumSupportedTshark: '4.4.0',
|
|
34
|
+
termsharkLatest: '2.4.0',
|
|
35
|
+
termsharkOptionalRisk: 'aged-release-managed',
|
|
36
|
+
sources: [
|
|
37
|
+
'https://www.wireshark.org/download.html',
|
|
38
|
+
'https://www.wireshark.org/docs/wsug_html_chunked/ChIntroReleaseLifeCycle.html',
|
|
39
|
+
'https://github.com/gcla/termshark/releases',
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
export function createDefaultProbeHost() {
|
|
43
|
+
return {
|
|
44
|
+
async isExecutable(file) {
|
|
45
|
+
try {
|
|
46
|
+
await access(file, constants.X_OK);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
async run(file, args, limits) {
|
|
54
|
+
const configRoot = await mkdtemp(path.join(tmpdir(), 'aiwg-network-probe-'));
|
|
55
|
+
try {
|
|
56
|
+
const result = await execFile(file, [...args], {
|
|
57
|
+
timeout: limits.timeoutMs,
|
|
58
|
+
maxBuffer: limits.maxBufferBytes,
|
|
59
|
+
killSignal: 'SIGKILL',
|
|
60
|
+
shell: false,
|
|
61
|
+
env: isolatedProbeEnv(configRoot),
|
|
62
|
+
});
|
|
63
|
+
return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
68
|
+
stdout: typeof error?.stdout === 'string' ? error.stdout : '',
|
|
69
|
+
stderr: typeof error?.stderr === 'string' ? error.stderr : String(error?.message ?? ''),
|
|
70
|
+
timedOut: error?.code !== 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' && error?.killed === true,
|
|
71
|
+
outputLimited: error?.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await rm(configRoot, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function probeNetworkAnalysisTools(options = {}, host = createDefaultProbeHost()) {
|
|
81
|
+
const tools = {
|
|
82
|
+
tshark: await probeTool(TOOL_SPECS.tshark, options, host),
|
|
83
|
+
capinfos: await probeTool(TOOL_SPECS.capinfos, options, host),
|
|
84
|
+
termshark: await probeTool(TOOL_SPECS.termshark, options, host),
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
schema: NETWORK_ANALYSIS_PROBE_SCHEMA,
|
|
88
|
+
generatedAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
89
|
+
status: overallStatus(tools),
|
|
90
|
+
tools,
|
|
91
|
+
provenance: {
|
|
92
|
+
configuredPaths: { ...(options.configuredPaths ?? {}) },
|
|
93
|
+
trustedSearchPaths: normalizeTrustedSearchPaths(options.trustedSearchPaths ?? []),
|
|
94
|
+
declaredProfilePaths: explicitAbsoluteList(options.declaredProfilePaths ?? []),
|
|
95
|
+
declaredConfigPaths: explicitAbsoluteList(options.declaredConfigPaths ?? []),
|
|
96
|
+
pathPolicy: 'explicit absolute configured paths take precedence; invalid explicit paths fail closed; ambient cwd and user PATH are never searched implicitly',
|
|
97
|
+
executionPolicy: 'bounded execFile execution only; isolated clean environment; no shell, installs, privilege changes, ambient profiles, user configs, or packet capture',
|
|
98
|
+
versionPolicy: CURRENT_NETWORK_ANALYSIS_VERSION_POLICY,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function probeTool(spec, options, host) {
|
|
103
|
+
const diagnostics = [];
|
|
104
|
+
const resolved = await resolveExecutable(spec.name, options, host, diagnostics);
|
|
105
|
+
if (!resolved)
|
|
106
|
+
return emptyTool(spec, diagnostics);
|
|
107
|
+
const versionLimits = limitsFor(spec.name, spec.versionArgs);
|
|
108
|
+
const versionResult = await host.run(resolved.path, spec.versionArgs, versionLimits);
|
|
109
|
+
const commands = [commandRecord(spec.versionArgs, versionResult, versionLimits)];
|
|
110
|
+
recordCommandFailure(spec.name, spec.versionArgs, versionResult, diagnostics);
|
|
111
|
+
const versionOutput = `${versionResult.stdout}\n${versionResult.stderr}`;
|
|
112
|
+
const version = commandSucceeded(versionResult) ? parseToolVersion(spec.name, versionOutput) : null;
|
|
113
|
+
if (!version)
|
|
114
|
+
diagnostics.push(`Could not parse ${spec.name} version output.`);
|
|
115
|
+
const capabilityOutputs = [];
|
|
116
|
+
for (const args of spec.capabilityCommands) {
|
|
117
|
+
const limits = limitsFor(spec.name, args);
|
|
118
|
+
const result = await host.run(resolved.path, args, limits);
|
|
119
|
+
commands.push(commandRecord(args, result, limits));
|
|
120
|
+
capabilityOutputs.push({ args: [...args], output: `${result.stdout}\n${result.stderr}` });
|
|
121
|
+
recordCommandFailure(spec.name, args, result, diagnostics);
|
|
122
|
+
}
|
|
123
|
+
const buildCapabilities = detectBuildCapabilities(versionOutput);
|
|
124
|
+
const featureDetection = detectFeatures(spec.name, capabilityOutputs);
|
|
125
|
+
featureDetection.captureFormats = await detectReadCaptureFormats(spec.name, resolved.path, host, commands, diagnostics);
|
|
126
|
+
const status = classifyTool(spec, version, featureDetection, diagnostics);
|
|
127
|
+
return {
|
|
128
|
+
name: spec.name,
|
|
129
|
+
status,
|
|
130
|
+
required: spec.required,
|
|
131
|
+
path: resolved.path,
|
|
132
|
+
trust: resolved.trust,
|
|
133
|
+
version,
|
|
134
|
+
buildCapabilities,
|
|
135
|
+
featureDetection,
|
|
136
|
+
commands,
|
|
137
|
+
diagnostics,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function resolveExecutable(name, options, host, diagnostics) {
|
|
141
|
+
const explicit = options.configuredPaths?.[name];
|
|
142
|
+
if (explicit !== undefined) {
|
|
143
|
+
if (!path.isAbsolute(explicit)) {
|
|
144
|
+
diagnostics.push(`Configured ${name} path is not absolute: ${explicit}`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (!await host.isExecutable(explicit)) {
|
|
148
|
+
diagnostics.push(`Configured ${name} path is not executable or does not exist: ${explicit}`);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return { path: explicit, trust: 'explicit-config' };
|
|
152
|
+
}
|
|
153
|
+
for (const directory of normalizeTrustedSearchPaths(options.trustedSearchPaths ?? [])) {
|
|
154
|
+
const candidate = path.join(directory, name);
|
|
155
|
+
if (await host.isExecutable(candidate))
|
|
156
|
+
return { path: candidate, trust: 'trusted-search-path' };
|
|
157
|
+
}
|
|
158
|
+
diagnostics.push(`No trusted executable found for ${name}.`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function normalizeTrustedSearchPaths(paths) {
|
|
162
|
+
return paths.filter(path.isAbsolute);
|
|
163
|
+
}
|
|
164
|
+
function explicitAbsoluteList(paths) {
|
|
165
|
+
return paths.filter(path.isAbsolute);
|
|
166
|
+
}
|
|
167
|
+
function emptyTool(spec, diagnostics) {
|
|
168
|
+
return {
|
|
169
|
+
name: spec.name,
|
|
170
|
+
status: 'missing',
|
|
171
|
+
required: spec.required,
|
|
172
|
+
path: null,
|
|
173
|
+
trust: null,
|
|
174
|
+
version: null,
|
|
175
|
+
buildCapabilities: [],
|
|
176
|
+
featureDetection: {
|
|
177
|
+
outputFormats: [],
|
|
178
|
+
statistics: [],
|
|
179
|
+
fields: [],
|
|
180
|
+
captureFormats: [],
|
|
181
|
+
writeFormats: [],
|
|
182
|
+
},
|
|
183
|
+
commands: [],
|
|
184
|
+
diagnostics,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function commandRecord(args, result, limits) {
|
|
188
|
+
return {
|
|
189
|
+
args: [...args],
|
|
190
|
+
exitCode: result.exitCode,
|
|
191
|
+
timedOut: result.timedOut === true,
|
|
192
|
+
outputLimited: result.outputLimited === true,
|
|
193
|
+
maxBufferBytes: limits.maxBufferBytes,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function limitsFor(name, args) {
|
|
197
|
+
if (name === 'tshark' && args.join(' ') === '-G fields') {
|
|
198
|
+
return { timeoutMs: 10000, maxBufferBytes: 32 * 1024 * 1024 };
|
|
199
|
+
}
|
|
200
|
+
if (name === 'tshark' && args.join(' ') === '-z help') {
|
|
201
|
+
return { timeoutMs: 5000, maxBufferBytes: 4 * 1024 * 1024 };
|
|
202
|
+
}
|
|
203
|
+
return { timeoutMs: 3000, maxBufferBytes: 2 * 1024 * 1024 };
|
|
204
|
+
}
|
|
205
|
+
function isolatedProbeEnv(configRoot) {
|
|
206
|
+
return {
|
|
207
|
+
XDG_CONFIG_HOME: configRoot,
|
|
208
|
+
WIRESHARK_CONFIG_DIR: configRoot,
|
|
209
|
+
TERM: 'dumb',
|
|
210
|
+
LANG: 'C',
|
|
211
|
+
LC_ALL: 'C',
|
|
212
|
+
PATH: '',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async function detectReadCaptureFormats(name, executable, host, commands, diagnostics) {
|
|
216
|
+
if (name === 'termshark')
|
|
217
|
+
return [];
|
|
218
|
+
const smokeRoot = await mkdtemp(path.join(tmpdir(), 'aiwg-network-capture-smoke-'));
|
|
219
|
+
try {
|
|
220
|
+
const captures = [
|
|
221
|
+
{ format: 'pcap', file: path.join(smokeRoot, 'empty.pcap'), content: emptyPcap() },
|
|
222
|
+
{ format: 'pcapng', file: path.join(smokeRoot, 'empty.pcapng'), content: emptyPcapng() },
|
|
223
|
+
];
|
|
224
|
+
await Promise.all(captures.map(capture => writeFile(capture.file, capture.content)));
|
|
225
|
+
const supported = [];
|
|
226
|
+
for (const capture of captures) {
|
|
227
|
+
const args = name === 'tshark' ? ['-r', capture.file, '-q'] : [capture.file];
|
|
228
|
+
const limits = limitsFor(name, ['read-smoke']);
|
|
229
|
+
const result = await host.run(executable, args, limits);
|
|
230
|
+
commands.push(commandRecord(args, result, limits));
|
|
231
|
+
recordCommandFailure(name, args, result, diagnostics);
|
|
232
|
+
if (commandSucceeded(result))
|
|
233
|
+
supported.push(capture.format);
|
|
234
|
+
}
|
|
235
|
+
return supported;
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await rm(smokeRoot, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function emptyPcap() {
|
|
242
|
+
return Buffer.from([
|
|
243
|
+
0xd4, 0xc3, 0xb2, 0xa1,
|
|
244
|
+
0x02, 0x00,
|
|
245
|
+
0x04, 0x00,
|
|
246
|
+
0x00, 0x00, 0x00, 0x00,
|
|
247
|
+
0x00, 0x00, 0x00, 0x00,
|
|
248
|
+
0xff, 0xff, 0x00, 0x00,
|
|
249
|
+
0x01, 0x00, 0x00, 0x00,
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
function emptyPcapng() {
|
|
253
|
+
return Buffer.from([
|
|
254
|
+
0x0a, 0x0d, 0x0d, 0x0a,
|
|
255
|
+
0x1c, 0x00, 0x00, 0x00,
|
|
256
|
+
0x4d, 0x3c, 0x2b, 0x1a,
|
|
257
|
+
0x01, 0x00,
|
|
258
|
+
0x00, 0x00,
|
|
259
|
+
0xff, 0xff, 0xff, 0xff,
|
|
260
|
+
0xff, 0xff, 0xff, 0xff,
|
|
261
|
+
0x1c, 0x00, 0x00, 0x00,
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
function parseToolVersion(name, output) {
|
|
265
|
+
const patterns = {
|
|
266
|
+
tshark: [/\bTShark \(Wireshark\) ([0-9]+(?:\.[0-9]+){1,2})\b/i, /\bTShark ([0-9]+(?:\.[0-9]+){1,2})\b/i],
|
|
267
|
+
capinfos: [/\bCapinfos \(Wireshark\) ([0-9]+(?:\.[0-9]+){1,2})\b/i, /\bCapinfos ([0-9]+(?:\.[0-9]+){1,2})\b/i],
|
|
268
|
+
termshark: [/\btermshark v?([0-9]+(?:\.[0-9]+){1,2})\b/i],
|
|
269
|
+
};
|
|
270
|
+
for (const pattern of patterns[name]) {
|
|
271
|
+
const match = output.match(pattern);
|
|
272
|
+
if (match?.[1])
|
|
273
|
+
return match[1];
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function detectBuildCapabilities(output) {
|
|
278
|
+
const capabilities = new Set();
|
|
279
|
+
const checks = [
|
|
280
|
+
['libpcap', /\bwith libpcap\b/i],
|
|
281
|
+
['nghttp2', /\bwith nghttp2\b/i],
|
|
282
|
+
['brotli', /\bwith brotli\b/i],
|
|
283
|
+
['zstd', /\bwith zstd\b/i],
|
|
284
|
+
['lua', /\bwith Lua\b/],
|
|
285
|
+
['gnutls', /\bwith GnuTLS\b/],
|
|
286
|
+
['maxminddb', /\bwith MaxMind DB\b/i],
|
|
287
|
+
];
|
|
288
|
+
for (const [capability, pattern] of checks) {
|
|
289
|
+
if (pattern.test(output))
|
|
290
|
+
capabilities.add(capability);
|
|
291
|
+
}
|
|
292
|
+
return [...capabilities].sort();
|
|
293
|
+
}
|
|
294
|
+
function detectFeatures(name, outputs) {
|
|
295
|
+
const outputFor = (args) => outputs
|
|
296
|
+
.filter(item => item.args.join(' ') === args.join(' '))
|
|
297
|
+
.map(item => item.output)
|
|
298
|
+
.join('\n');
|
|
299
|
+
return {
|
|
300
|
+
outputFormats: name === 'tshark' ? detectOutputFormats(outputFor(['-h'])) : [],
|
|
301
|
+
statistics: name === 'tshark' ? detectStatistics(outputFor(['-z', 'help'])) : [],
|
|
302
|
+
fields: name === 'tshark' ? detectFields(outputFor(['-G', 'fields'])) : [],
|
|
303
|
+
captureFormats: [],
|
|
304
|
+
writeFormats: name === 'tshark' ? detectFormats(outputFor(['-F'])) : [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function detectOutputFormats(output) {
|
|
308
|
+
return uniqueMatches(output, /\b(?:ek|fields|json|jsonraw|pdml|ps|psml|tabs|text)\b/g);
|
|
309
|
+
}
|
|
310
|
+
function detectStatistics(output) {
|
|
311
|
+
return uniqueMatches(output, /\b(?:conv|endpoints|io,stat|protocols|expert|flow|http|http2|dns|icmp|udp|tcp)\b/g);
|
|
312
|
+
}
|
|
313
|
+
function detectFields(output) {
|
|
314
|
+
const fields = new Set();
|
|
315
|
+
for (const line of output.split(/\r?\n/)) {
|
|
316
|
+
const columns = line.split('\t');
|
|
317
|
+
if (columns[0] === 'F' && columns[2])
|
|
318
|
+
fields.add(columns[2]);
|
|
319
|
+
}
|
|
320
|
+
return [...fields].sort();
|
|
321
|
+
}
|
|
322
|
+
function detectFormats(output) {
|
|
323
|
+
const formats = new Set();
|
|
324
|
+
for (const token of uniqueMatches(output, /\b(?:pcapng|pcap|erf|snoop|ngsniffer|netmon|libpcap|modpcap)\b/gi)) {
|
|
325
|
+
formats.add(token.toLowerCase());
|
|
326
|
+
}
|
|
327
|
+
return [...formats].sort();
|
|
328
|
+
}
|
|
329
|
+
function commandSucceeded(result) {
|
|
330
|
+
return result.exitCode === 0 && result.timedOut !== true && result.outputLimited !== true;
|
|
331
|
+
}
|
|
332
|
+
function recordCommandFailure(name, args, result, diagnostics) {
|
|
333
|
+
const command = `${name} ${args.map(arg => path.basename(arg) === arg ? arg : '<temp>').join(' ')}`;
|
|
334
|
+
if (result.timedOut)
|
|
335
|
+
diagnostics.push(`${command} timed out.`);
|
|
336
|
+
if (result.outputLimited)
|
|
337
|
+
diagnostics.push(`${command} exceeded the output limit.`);
|
|
338
|
+
if (result.exitCode !== 0 && !isExpectedNonZeroCommand(name, args)) {
|
|
339
|
+
diagnostics.push(`${command} exited with code ${result.exitCode ?? 'unknown'}.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function isExpectedNonZeroCommand(name, args) {
|
|
343
|
+
return name === 'tshark' && args.join(' ') === '-F';
|
|
344
|
+
}
|
|
345
|
+
function uniqueMatches(output, pattern) {
|
|
346
|
+
return [...new Set([...output.matchAll(pattern)].map(match => match[0]))].sort();
|
|
347
|
+
}
|
|
348
|
+
function classifyTool(spec, version, features, diagnostics) {
|
|
349
|
+
if (!version)
|
|
350
|
+
return 'unsupported';
|
|
351
|
+
if (diagnostics.some(diagnostic => /exited with code|timed out|exceeded the output limit/.test(diagnostic))) {
|
|
352
|
+
return 'partially_capable';
|
|
353
|
+
}
|
|
354
|
+
if (spec.name === 'termshark')
|
|
355
|
+
return version === CURRENT_NETWORK_ANALYSIS_VERSION_POLICY.termsharkLatest
|
|
356
|
+
? 'supported'
|
|
357
|
+
: 'partially_capable';
|
|
358
|
+
if (!CURRENT_NETWORK_ANALYSIS_VERSION_POLICY.maintainedBranches.some(branch => version.startsWith(`${branch}.`))) {
|
|
359
|
+
diagnostics.push(`${spec.name} ${version} is outside the maintained Wireshark branches.`);
|
|
360
|
+
return 'unsupported';
|
|
361
|
+
}
|
|
362
|
+
const missingRequiredFeatures = spec.name === 'tshark'
|
|
363
|
+
? requiredTsharkFeaturesMissing(features)
|
|
364
|
+
: requiredCapinfosFeaturesMissing(features);
|
|
365
|
+
if (missingRequiredFeatures.length > 0) {
|
|
366
|
+
diagnostics.push(`Missing feature detections: ${missingRequiredFeatures.join(', ')}.`);
|
|
367
|
+
return 'partially_capable';
|
|
368
|
+
}
|
|
369
|
+
return 'supported';
|
|
370
|
+
}
|
|
371
|
+
function requiredTsharkFeaturesMissing(features) {
|
|
372
|
+
const missing = [];
|
|
373
|
+
for (const format of ['json', 'fields']) {
|
|
374
|
+
if (!features.outputFormats.includes(format))
|
|
375
|
+
missing.push(`output:${format}`);
|
|
376
|
+
}
|
|
377
|
+
if (!features.statistics.includes('conv'))
|
|
378
|
+
missing.push('statistics:conv');
|
|
379
|
+
if (!features.fields.includes('frame.time_epoch'))
|
|
380
|
+
missing.push('field:frame.time_epoch');
|
|
381
|
+
for (const format of ['pcap', 'pcapng']) {
|
|
382
|
+
if (!features.captureFormats.includes(format))
|
|
383
|
+
missing.push(`capture-read:${format}`);
|
|
384
|
+
}
|
|
385
|
+
return missing;
|
|
386
|
+
}
|
|
387
|
+
function requiredCapinfosFeaturesMissing(features) {
|
|
388
|
+
const missing = [];
|
|
389
|
+
for (const format of ['pcap', 'pcapng']) {
|
|
390
|
+
if (!features.captureFormats.includes(format))
|
|
391
|
+
missing.push(`capture-read:${format}`);
|
|
392
|
+
}
|
|
393
|
+
return missing;
|
|
394
|
+
}
|
|
395
|
+
function overallStatus(tools) {
|
|
396
|
+
const required = [tools.tshark, tools.capinfos];
|
|
397
|
+
if (required.some(tool => tool.status === 'missing'))
|
|
398
|
+
return 'missing';
|
|
399
|
+
if (required.some(tool => tool.status === 'unsupported'))
|
|
400
|
+
return 'unsupported';
|
|
401
|
+
if (required.some(tool => tool.status === 'partially_capable'))
|
|
402
|
+
return 'partially_capable';
|
|
403
|
+
return 'supported';
|
|
404
|
+
}
|
|
405
|
+
//# sourceMappingURL=probe.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
export const ANALYSIS_RECIPE_SCHEMA_VERSION = 'network-analysis.analysis-recipe/v1';
|
|
3
|
+
export class RecipeCompatibilityError extends Error {
|
|
4
|
+
diagnostics;
|
|
5
|
+
constructor(message, diagnostics) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'RecipeCompatibilityError';
|
|
8
|
+
this.diagnostics = diagnostics;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function compileAnalysisRecipe(document, capability) {
|
|
12
|
+
assertRecipeIdentity(document);
|
|
13
|
+
const diagnostics = [];
|
|
14
|
+
const tshark = document.compatibility.minimum_tools.find(tool => tool.name === 'tshark');
|
|
15
|
+
if (!tshark?.required)
|
|
16
|
+
throw new RecipeCompatibilityError('Recipe must require TShark.', ['Missing required TShark compatibility entry.']);
|
|
17
|
+
if (!semver.valid(capability.tsharkVersion) || !semver.satisfies(capability.tsharkVersion, tshark.version_range, { includePrerelease: true })) {
|
|
18
|
+
throw new RecipeCompatibilityError(`TShark ${capability.tsharkVersion} is incompatible with ${document.recipe_id}.`, [`Install a maintained TShark matching ${tshark.version_range}; unsupported major versions fail closed.`]);
|
|
19
|
+
}
|
|
20
|
+
if (document.filters.capture_filters.length > 0) {
|
|
21
|
+
throw new RecipeCompatibilityError('Offline recipes cannot apply capture filters after collection.', [
|
|
22
|
+
'Select a saved capture and use only Wireshark display filters during analysis.',
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
const available = new Set(capability.availableFields);
|
|
26
|
+
const fallbackByField = new Map((document.compatibility.field_fallbacks ?? []).map(fallback => [fallback.field, fallback]));
|
|
27
|
+
const selectedFields = [];
|
|
28
|
+
const usedFallbacks = [];
|
|
29
|
+
for (const request of document.requested_output.fields) {
|
|
30
|
+
if (available.has(request.name)) {
|
|
31
|
+
selectedFields.push(request.name);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const fallback = fallbackByField.get(request.name);
|
|
35
|
+
if (fallback?.behavior === 'use-fallback' && fallback.fallback_field && available.has(fallback.fallback_field)) {
|
|
36
|
+
selectedFields.push(fallback.fallback_field);
|
|
37
|
+
diagnostics.push(`${fallback.diagnostic} Selected ${fallback.fallback_field}.`);
|
|
38
|
+
usedFallbacks.push({ field: request.name, replacement: fallback.fallback_field });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!request.required && fallback?.behavior !== 'fail-required') {
|
|
42
|
+
diagnostics.push(fallback?.diagnostic ?? `Optional field ${request.name} is unavailable and was omitted.`);
|
|
43
|
+
usedFallbacks.push({ field: request.name });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
throw new RecipeCompatibilityError(`Required field ${request.name} is unavailable for ${document.recipe_id}.`, [
|
|
47
|
+
fallback?.diagnostic ?? `Probe TShark fields and select a compatible recipe version; ${request.name} has no declared fallback.`,
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
const displayFilter = document.filters.display_filters.map(filter => `(${filter.expression})`).join(' && ');
|
|
51
|
+
return {
|
|
52
|
+
recipe: {
|
|
53
|
+
id: document.recipe_id.replace(/^analysis-recipe:/, ''),
|
|
54
|
+
version: document.version,
|
|
55
|
+
...(displayFilter ? { displayFilter } : {}),
|
|
56
|
+
fields: [...new Set(selectedFields)],
|
|
57
|
+
},
|
|
58
|
+
limits: {
|
|
59
|
+
inputBytes: document.limits.max_capture_bytes,
|
|
60
|
+
packets: document.limits.max_frames,
|
|
61
|
+
outputBytes: document.limits.max_output_bytes,
|
|
62
|
+
timeoutMs: document.limits.max_duration_seconds * 1000,
|
|
63
|
+
},
|
|
64
|
+
sensitivity: document.sensitivity,
|
|
65
|
+
statistics: document.requested_output.statistics,
|
|
66
|
+
interpretation: document.interpretation,
|
|
67
|
+
diagnostics,
|
|
68
|
+
provenance: {
|
|
69
|
+
recipeId: document.recipe_id,
|
|
70
|
+
recipeVersion: document.version,
|
|
71
|
+
toolVersion: capability.tsharkVersion,
|
|
72
|
+
selectedFields: [...new Set(selectedFields)],
|
|
73
|
+
displayFilter,
|
|
74
|
+
fallbacks: usedFallbacks,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function assertRecipeIdentity(document) {
|
|
79
|
+
if (document.schema_version !== ANALYSIS_RECIPE_SCHEMA_VERSION || document.kind !== 'AnalysisRecipe'
|
|
80
|
+
|| document.compatibility.contract !== ANALYSIS_RECIPE_SCHEMA_VERSION
|
|
81
|
+
|| document.compatibility.unsupported_major_policy !== 'fail-closed') {
|
|
82
|
+
throw new RecipeCompatibilityError('Unsupported analysis recipe contract.', ['Use network-analysis.analysis-recipe/v1; unsupported major versions fail closed.']);
|
|
83
|
+
}
|
|
84
|
+
if (!/^analysis-recipe:[A-Za-z0-9][A-Za-z0-9._-]*$/.test(document.recipe_id) || !semver.valid(document.version)) {
|
|
85
|
+
throw new RecipeCompatibilityError('Recipe identity or version is invalid.', ['Use a scoped recipe_id and semantic version.']);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=recipes.js.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { formatEvidenceCitation, validateEvidenceReferences } from './citations.js';
|
|
2
|
+
import { assertLocalOutputAllowed } from './governance.js';
|
|
3
|
+
export const RESEARCH_PACKET_SOURCE_SCHEMA = 'aiwg.research.packet-evidence-source.v1';
|
|
4
|
+
export function inductPacketEvidenceSource(input) {
|
|
5
|
+
validateResearchInput(input);
|
|
6
|
+
validateEvidenceReferences(input.evidence);
|
|
7
|
+
const captureDigest = String(input.evidence.capture.capture_digest);
|
|
8
|
+
const rawCapture = rawCaptureDecision(input.evidence, input.rawCaptureApproval);
|
|
9
|
+
const timestamps = frameTimestampIndex(input.evidence);
|
|
10
|
+
const analysisContexts = (input.evidence.capture.analysis_contexts ?? []).map(context => ({
|
|
11
|
+
contextDigest: requiredString(context.context_digest, 'Analysis context digest'),
|
|
12
|
+
tool: {
|
|
13
|
+
name: requiredString(context.tool?.name, 'Analysis tool name'),
|
|
14
|
+
version: requiredString(context.tool?.version, 'Analysis tool version'),
|
|
15
|
+
executablePath: requiredString(context.tool?.executable_path, 'Analysis tool path'),
|
|
16
|
+
},
|
|
17
|
+
profile: requiredString(context.profile, 'Analysis profile'),
|
|
18
|
+
displayFilterDigest: requiredString(context.display_filter_digest, 'Display-filter digest'),
|
|
19
|
+
captureFilterDigest: requiredString(context.capture_filter_digest, 'Capture-filter digest'),
|
|
20
|
+
configDigests: stringArray(context.config_digests, 'Config digests'),
|
|
21
|
+
commandArgv: stringArray(context.command_argv, 'Analysis argv'),
|
|
22
|
+
}));
|
|
23
|
+
const claims = input.evidence.evidence_items.map((item, index) => researchClaim(item, index, timestamps));
|
|
24
|
+
const provenance = input.evidence.provenance;
|
|
25
|
+
const limitations = [
|
|
26
|
+
`Capture analysis status: ${input.evidence.status}`,
|
|
27
|
+
`Representativeness: ${input.collection.representativeness}`,
|
|
28
|
+
`Clock source: ${input.collection.clockSource}; source timezone: ${input.collection.timezone}`,
|
|
29
|
+
`Missing traffic: ${input.collection.missingTraffic}`,
|
|
30
|
+
`Encryption visibility: ${input.collection.encryptionVisibility}`,
|
|
31
|
+
...input.collection.analystLimitations.map(value => `Analyst limitation: ${value}`),
|
|
32
|
+
];
|
|
33
|
+
return {
|
|
34
|
+
schema: RESEARCH_PACKET_SOURCE_SCHEMA,
|
|
35
|
+
refId: input.refId,
|
|
36
|
+
title: input.title.trim(),
|
|
37
|
+
sourceType: 'packet-evidence',
|
|
38
|
+
scholarlyClassification: 'non-scholarly-observational-evidence',
|
|
39
|
+
quality: {
|
|
40
|
+
grade: 'not-applicable',
|
|
41
|
+
framework: 'packet-evidence-limitations',
|
|
42
|
+
reason: 'GRADE study ratings do not apply to a collected network trace; assess authority, integrity, scope, and limitations instead.',
|
|
43
|
+
},
|
|
44
|
+
evidenceBundle: {
|
|
45
|
+
bundleId: input.evidence.bundle_id,
|
|
46
|
+
status: input.evidence.status,
|
|
47
|
+
captureDigest,
|
|
48
|
+
createdAt: input.evidence.created_at,
|
|
49
|
+
},
|
|
50
|
+
collection: structuredClone(input.collection),
|
|
51
|
+
analysisContexts,
|
|
52
|
+
derivedArtifacts: input.evidence.artifacts.map(sanitizeDerivedArtifact),
|
|
53
|
+
rawCapture,
|
|
54
|
+
claims,
|
|
55
|
+
provenance: {
|
|
56
|
+
actor: requiredString(provenance.actor, 'Provenance actor'),
|
|
57
|
+
activity: requiredString(provenance.activity, 'Provenance activity'),
|
|
58
|
+
startedAt: requiredString(provenance.started_at, 'Provenance start'),
|
|
59
|
+
endedAt: requiredString(provenance.ended_at, 'Provenance end'),
|
|
60
|
+
recipeId: requiredString(provenance.recipe_id, 'Provenance recipe'),
|
|
61
|
+
authorizationRefs: stringArray(provenance.authorization_refs, 'Authorization references'),
|
|
62
|
+
},
|
|
63
|
+
limitations,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function sanitizeDerivedArtifact(artifact) {
|
|
67
|
+
const handling = artifact.data_handling;
|
|
68
|
+
return {
|
|
69
|
+
artifactId: requiredString(artifact.artifact_id, 'Derived artifact ID'),
|
|
70
|
+
mediaType: requiredString(artifact.media_type, 'Derived artifact media type'),
|
|
71
|
+
uri: requiredString(artifact.uri, 'Derived artifact URI'),
|
|
72
|
+
digest: {
|
|
73
|
+
algorithm: requiredString(artifact.digest?.algorithm, 'Derived artifact digest algorithm'),
|
|
74
|
+
value: requiredString(artifact.digest?.value, 'Derived artifact digest value'),
|
|
75
|
+
},
|
|
76
|
+
dataHandling: {
|
|
77
|
+
sensitivity: requiredString(handling?.sensitivity, 'Derived artifact sensitivity'),
|
|
78
|
+
payloadContent: requiredString(handling?.payload_content, 'Derived artifact payload declaration'),
|
|
79
|
+
redaction: {
|
|
80
|
+
state: requiredString(handling?.redaction?.state, 'Derived artifact redaction state'),
|
|
81
|
+
method: requiredString(handling?.redaction?.method, 'Derived artifact redaction method'),
|
|
82
|
+
redactedFields: stringArray(handling?.redaction?.redacted_fields, 'Derived artifact redacted fields'),
|
|
83
|
+
},
|
|
84
|
+
retentionClass: requiredString(handling?.retention?.class, 'Derived artifact retention class'),
|
|
85
|
+
disclosureState: requiredString(handling?.disclosure?.state, 'Derived artifact disclosure state'),
|
|
86
|
+
allowedAudiences: stringArray(handling?.disclosure?.allowed_audiences, 'Derived artifact allowed audiences'),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function researchClaim(item, index, timestamps) {
|
|
91
|
+
const basis = requiredString(item.basis, `Evidence item ${index + 1} basis`);
|
|
92
|
+
if (basis !== 'observation' && basis !== 'inference')
|
|
93
|
+
throw new Error(`Evidence item ${index + 1} must distinguish observation from inference`);
|
|
94
|
+
const citations = item.citations.map(citation => {
|
|
95
|
+
const frame = citation.locator.type === 'frame' ? citation.locator.frame_number : frameForStream(item.citations);
|
|
96
|
+
const observedAt = frame === null ? null : timestamps.get(frame) ?? null;
|
|
97
|
+
return {
|
|
98
|
+
citation: formatEvidenceCitation(citation),
|
|
99
|
+
captureDigest: citation.capture_digest,
|
|
100
|
+
locator: structuredClone(citation.locator),
|
|
101
|
+
observedAt,
|
|
102
|
+
timeBasis: observedAt === null ? 'not-recorded' : 'capture-frame-time-utc',
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
claimId: requiredString(item.evidence_id, `Evidence item ${index + 1} ID`),
|
|
107
|
+
kind: basis,
|
|
108
|
+
statement: requiredString(item.statement, `Evidence item ${index + 1} statement`),
|
|
109
|
+
confidence: requiredString(item.confidence, `Evidence item ${index + 1} confidence`),
|
|
110
|
+
citations,
|
|
111
|
+
inferenceInputs: basis === 'inference' ? stringArray(item.inference?.inputs, 'Inference inputs') : [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function rawCaptureDecision(evidence, approval) {
|
|
115
|
+
if (!approval)
|
|
116
|
+
return { included: false };
|
|
117
|
+
const captureDigest = String(evidence.capture.capture_digest);
|
|
118
|
+
if (!approval.approvalRef.trim() || !approval.approvedBy.trim() || !approval.basis.trim() || approval.captureDigest !== captureDigest) {
|
|
119
|
+
throw new Error('Raw capture research inclusion requires an exact, complete approval record');
|
|
120
|
+
}
|
|
121
|
+
assertLocalOutputAllowed(approval.policy, { content: ['metadata', 'raw-packets'], payloadOptIn: approval.payloadOptIn });
|
|
122
|
+
return {
|
|
123
|
+
included: true,
|
|
124
|
+
digest: captureDigest,
|
|
125
|
+
sourceUri: requiredString(evidence.capture.source_uri, 'Capture source URI'),
|
|
126
|
+
approvalRef: approval.approvalRef,
|
|
127
|
+
approvedBy: approval.approvedBy,
|
|
128
|
+
basis: approval.basis,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function frameTimestampIndex(evidence) {
|
|
132
|
+
const result = new Map();
|
|
133
|
+
for (const item of evidence.evidence_items) {
|
|
134
|
+
const fields = Array.isArray(item.observed_fields) ? item.observed_fields : [];
|
|
135
|
+
const frame = Number(fields.find(field => field.name === 'frame.number')?.value);
|
|
136
|
+
const epoch = Number(fields.find(field => field.name === 'frame.time_epoch')?.value);
|
|
137
|
+
if (Number.isSafeInteger(frame) && frame > 0 && Number.isFinite(epoch) && epoch >= 0) {
|
|
138
|
+
result.set(frame, new Date(epoch * 1000).toISOString());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
function frameForStream(citations) {
|
|
144
|
+
const frame = citations.find(citation => citation.locator.type === 'frame');
|
|
145
|
+
return frame?.locator.type === 'frame' ? frame.locator.frame_number : null;
|
|
146
|
+
}
|
|
147
|
+
function validateResearchInput(input) {
|
|
148
|
+
if (!/^REF-[0-9]{3,}$/.test(input.refId))
|
|
149
|
+
throw new Error('Packet evidence research source requires a REF-NNN identifier');
|
|
150
|
+
requiredString(input.title, 'Research source title');
|
|
151
|
+
if (input.evidence.kind !== 'PacketEvidenceBundle' || input.evidence.status === 'error') {
|
|
152
|
+
throw new Error('Research induction requires a usable PacketEvidenceBundle');
|
|
153
|
+
}
|
|
154
|
+
for (const [label, value] of Object.entries({
|
|
155
|
+
owner: input.collection.owner,
|
|
156
|
+
authority: input.collection.authorityRef,
|
|
157
|
+
purpose: input.collection.purpose,
|
|
158
|
+
representativeness: input.collection.representativeness,
|
|
159
|
+
clock: input.collection.clockSource,
|
|
160
|
+
timezone: input.collection.timezone,
|
|
161
|
+
missingTraffic: input.collection.missingTraffic,
|
|
162
|
+
encryption: input.collection.encryptionVisibility,
|
|
163
|
+
}))
|
|
164
|
+
requiredString(value, `Collection ${label}`);
|
|
165
|
+
if (!Array.isArray(input.collection.analystLimitations) || input.collection.analystLimitations.length === 0) {
|
|
166
|
+
throw new Error('At least one analyst limitation is required');
|
|
167
|
+
}
|
|
168
|
+
stringArray(input.collection.analystLimitations, 'Analyst limitations');
|
|
169
|
+
}
|
|
170
|
+
function requiredString(value, label) {
|
|
171
|
+
if (typeof value !== 'string' || !value.trim() || value.includes('\0'))
|
|
172
|
+
throw new Error(`${label} is required`);
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function stringArray(value, label) {
|
|
176
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || !item.trim() || item.includes('\0'))) {
|
|
177
|
+
throw new Error(`${label} must contain non-empty strings`);
|
|
178
|
+
}
|
|
179
|
+
return [...value];
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=research.js.map
|