@duckcodeailabs/dql-cli 1.11.1 → 1.11.2
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/dist/assets/dql-notebook/assets/{index-CxVzobkk.js → index-DotkS-Xi.js} +146 -146
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/block-studio-import.js +2 -1
- package/dist/block-studio-import.js.map +1 -1
- package/dist/local-runtime.d.ts +9 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +195 -81
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/package.json +10 -10
package/dist/local-runtime.js
CHANGED
|
@@ -1811,6 +1811,20 @@ export async function startLocalServer(opts) {
|
|
|
1811
1811
|
status: 'blocked',
|
|
1812
1812
|
trustState: 'blocked',
|
|
1813
1813
|
stopReason: 'blocked',
|
|
1814
|
+
artifacts: [agentRunArtifact('answer', 'AI answer provider failed', {
|
|
1815
|
+
kind: 'no_answer',
|
|
1816
|
+
refusalCode: 'provider_error',
|
|
1817
|
+
text: message,
|
|
1818
|
+
answer: message,
|
|
1819
|
+
...(routeDecision?.resolvedAnalyticalPlan
|
|
1820
|
+
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
1821
|
+
: {}),
|
|
1822
|
+
providerFailure: {
|
|
1823
|
+
code: 'AI_PROVIDER_FAILURE',
|
|
1824
|
+
message,
|
|
1825
|
+
recoverable: true,
|
|
1826
|
+
},
|
|
1827
|
+
}, undefined, 'blocked')],
|
|
1814
1828
|
evaluations: [
|
|
1815
1829
|
agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
|
|
1816
1830
|
agentRunEvaluation('ai-provider', 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
|
|
@@ -1836,12 +1850,12 @@ export async function startLocalServer(opts) {
|
|
|
1836
1850
|
&& !isExploratory;
|
|
1837
1851
|
const isProviderError = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'provider_error';
|
|
1838
1852
|
const isAnalyticalFailure = Boolean(governedAnswer.analyticalFailure);
|
|
1839
|
-
const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure;
|
|
1840
1853
|
// AGT-004: a rejected attribution/export/proof policy is a deliberate
|
|
1841
1854
|
// governance boundary, not an ambiguous user question and not a repairable
|
|
1842
1855
|
// retrieval miss. Keep the precise policy detail visible, but never burn two
|
|
1843
1856
|
// more provider calls retrying the same incompatible candidate.
|
|
1844
1857
|
const isPolicyBlocked = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked';
|
|
1858
|
+
const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure || isPolicyBlocked;
|
|
1845
1859
|
// The model tried to compose a governed query and declined despite having usable
|
|
1846
1860
|
// context (e.g. it wasn't confident about a multi-table join). The answer loop
|
|
1847
1861
|
// has already spent its one evidence-aware repair. Keep this terminal and
|
|
@@ -1933,7 +1947,16 @@ export async function startLocalServer(opts) {
|
|
|
1933
1947
|
: [{ id: 'review-analytical-failure', label: 'Review the plan, DQL, SQL, and safe repair actions', route: 'blocked' }]
|
|
1934
1948
|
: isProviderError
|
|
1935
1949
|
? [{ id: 'retry-after-provider', label: 'Retry after fixing the AI provider', route: 'generated_answer' }]
|
|
1936
|
-
:
|
|
1950
|
+
: isPolicyBlocked
|
|
1951
|
+
? [
|
|
1952
|
+
...(governedAnswer.dqlArtifact
|
|
1953
|
+
? [{ id: 'edit_dql', label: 'Open DQL to repair', route: 'dql_block_draft', artifactKind: 'dql_block_draft' }]
|
|
1954
|
+
: []),
|
|
1955
|
+
...(sql
|
|
1956
|
+
? [{ id: 'open_sql_notebook', label: 'Open SQL in Notebook', route: 'sql_cell', artifactKind: 'sql_cell' }]
|
|
1957
|
+
: []),
|
|
1958
|
+
]
|
|
1959
|
+
: [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }];
|
|
1937
1960
|
const nextActions = needsClarification
|
|
1938
1961
|
? [{ id: 'clarify', label: 'Clarify question', route: 'generated_answer' }]
|
|
1939
1962
|
: isTerminalFailure
|
|
@@ -1955,7 +1978,11 @@ export async function startLocalServer(opts) {
|
|
|
1955
1978
|
answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
|
|
1956
1979
|
answerTier: governedAnswer.route?.tier,
|
|
1957
1980
|
summary: isTerminalFailure
|
|
1958
|
-
?
|
|
1981
|
+
? governedAnswer.analyticalFailure?.message
|
|
1982
|
+
?? governedAnswer.executionError
|
|
1983
|
+
?? governedAnswer.answer
|
|
1984
|
+
?? governedAnswer.text
|
|
1985
|
+
?? 'The governed query could not be executed.'
|
|
1959
1986
|
: governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
|
|
1960
1987
|
answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
|
|
1961
1988
|
...(governedAnswer.clarificationOptions?.length
|
|
@@ -2384,87 +2411,88 @@ export async function startLocalServer(opts) {
|
|
|
2384
2411
|
],
|
|
2385
2412
|
};
|
|
2386
2413
|
},
|
|
2387
|
-
dql_block_draft: async ({ request, routeDecision, attempt, repairHint }) => {
|
|
2414
|
+
dql_block_draft: async ({ request, routeDecision, attempt, repairHint, emit, runId, maxRepairAttempts, }) => {
|
|
2388
2415
|
const contextRecord = agentRunRecord(request.conversationContext);
|
|
2389
2416
|
const carriedDqlArtifact = normalizeDqlArtifactReference(contextRecord?.dqlArtifact)
|
|
2390
2417
|
?? normalizeDqlArtifactReference(request.workspaceContext?.dqlArtifact);
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
tags: ['agent-run', 'review-required'],
|
|
2401
|
-
contextPackId: agentRunString(contextRecord?.contextPackId),
|
|
2402
|
-
routeIntent: agentRunString(contextRecord?.route),
|
|
2403
|
-
sourceBlock: carriedDqlArtifact.sourcePath,
|
|
2404
|
-
}, semanticLayer);
|
|
2405
|
-
const candidate = session.candidates[0];
|
|
2406
|
-
const validation = candidate?.validation && typeof candidate.validation === 'object'
|
|
2407
|
-
? candidate.validation
|
|
2408
|
-
: undefined;
|
|
2409
|
-
const ready = validation?.valid !== false;
|
|
2410
|
-
const payload = {
|
|
2411
|
-
target: 'block',
|
|
2412
|
-
name: candidate?.name ?? carriedDqlArtifact.name ?? 'DQL block draft',
|
|
2413
|
-
path: candidate?.draftSave?.path ?? candidate?.savedPath,
|
|
2414
|
-
importId: session.id,
|
|
2415
|
-
candidateId: candidate?.id,
|
|
2416
|
-
source: candidate?.dqlSource,
|
|
2417
|
-
dqlSource: candidate?.dqlSource,
|
|
2418
|
-
dqlArtifact: carriedDqlArtifact,
|
|
2419
|
-
certifierVerdict: {
|
|
2420
|
-
ready,
|
|
2421
|
-
diagnostics: validation?.diagnostics ?? [],
|
|
2422
|
-
},
|
|
2423
|
-
generation: session.generation,
|
|
2424
|
-
};
|
|
2425
|
-
return {
|
|
2426
|
-
summary: ready
|
|
2427
|
-
? 'Saved the generated DQL artifact as a review-required block draft.'
|
|
2428
|
-
: 'Saved the generated DQL artifact as a block draft with review blockers or warnings.',
|
|
2429
|
-
artifacts: [agentRunArtifact('dql_block_draft', payload.name, payload, payload.path)],
|
|
2418
|
+
// UI-016 / E2E-015: a cold Block AI request uses the same governed answer
|
|
2419
|
+
// pipeline as Ask and Notebook. Generation is write-free. The returned
|
|
2420
|
+
// ownerless artifact is persisted only when the user explicitly chooses
|
|
2421
|
+
// Add to Block Studio.
|
|
2422
|
+
const governedResult = carriedDqlArtifact && (attempt ?? 0) === 0
|
|
2423
|
+
? {
|
|
2424
|
+
summary: 'Reused the governed answer artifact as a transient Block AI draft.',
|
|
2425
|
+
answer: 'Prepared the existing governed DQL as an ownerless review draft. No file was changed.',
|
|
2426
|
+
artifacts: [agentRunArtifact('answer', carriedDqlArtifact.name ?? 'Governed DQL answer', { dqlArtifact: carriedDqlArtifact }, undefined, carriedDqlArtifact.trustState)],
|
|
2430
2427
|
evaluations: [
|
|
2431
|
-
agentRunEvaluation('
|
|
2432
|
-
agentRunEvaluation('dql-artifact-reuse', 'Used generated DQL artifact', true, 'info', 'Persisted the existing DQL artifact without regenerating it from SQL.', { importId: session.id, path: payload.path }),
|
|
2433
|
-
agentRunEvaluation('certification-boundary', 'Certification boundary', ready, 'warning', ready
|
|
2434
|
-
? 'The draft has no automatic validation blockers, but certification still requires human review.'
|
|
2435
|
-
: 'The draft has validation blockers that must be resolved before certification review.', payload),
|
|
2436
|
-
],
|
|
2437
|
-
nextActions: [
|
|
2438
|
-
{ id: 'open-review', label: 'Open review checklist', artifactKind: 'dql_block_draft' },
|
|
2439
|
-
{ id: 'build-app', label: 'Build app from block', route: 'app_build', artifactKind: 'app_draft' },
|
|
2428
|
+
agentRunEvaluation('dql-artifact-reuse', 'Reused governed DQL artifact', true, 'info', 'Reused the exact answer artifact without regenerating SQL or writing a block file.'),
|
|
2440
2429
|
],
|
|
2441
|
-
}
|
|
2430
|
+
}
|
|
2431
|
+
: await answerRunExecutor({
|
|
2432
|
+
request,
|
|
2433
|
+
route: 'generated_answer',
|
|
2434
|
+
routeDecision,
|
|
2435
|
+
attempt,
|
|
2436
|
+
repairHint,
|
|
2437
|
+
emit,
|
|
2438
|
+
runId,
|
|
2439
|
+
maxRepairAttempts,
|
|
2440
|
+
});
|
|
2441
|
+
if (governedResult.status === 'blocked' || governedResult.status === 'needs_clarification') {
|
|
2442
|
+
return governedResult;
|
|
2443
|
+
}
|
|
2444
|
+
const governedPayload = (governedResult.artifacts ?? [])
|
|
2445
|
+
.map((artifact) => agentRunRecord(artifact.payload))
|
|
2446
|
+
.find((payload) => Boolean(normalizeDqlArtifactReference(payload?.dqlArtifact)
|
|
2447
|
+
?? normalizeDqlArtifactReference(payload)));
|
|
2448
|
+
const directDqlArtifact = normalizeDqlArtifactReference(governedPayload);
|
|
2449
|
+
const governedAnswer = governedPayload
|
|
2450
|
+
? directDqlArtifact
|
|
2451
|
+
? { dqlArtifact: directDqlArtifact }
|
|
2452
|
+
: governedPayload
|
|
2453
|
+
: undefined;
|
|
2454
|
+
const dqlArtifact = governedAnswer
|
|
2455
|
+
? ownerlessReviewDqlArtifactFromAnswer(governedAnswer, request.question)
|
|
2456
|
+
: undefined;
|
|
2457
|
+
if (!dqlArtifact) {
|
|
2458
|
+
return governedResult;
|
|
2442
2459
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2460
|
+
const sql = dqlArtifact.compiledSql
|
|
2461
|
+
?? agentRunString(governedPayload?.proposedSql)
|
|
2462
|
+
?? agentRunString(governedPayload?.sql);
|
|
2463
|
+
const result = {
|
|
2464
|
+
target: 'block',
|
|
2465
|
+
name: dqlArtifact.name ?? deriveGeneratedDraftSlug(request.question),
|
|
2466
|
+
source: dqlArtifact.source,
|
|
2467
|
+
dqlSource: dqlArtifact.source,
|
|
2468
|
+
dqlArtifact,
|
|
2469
|
+
...(sql ? { sql, sqlPreview: sql } : {}),
|
|
2470
|
+
...(governedPayload?.result ? { result: governedPayload.result } : {}),
|
|
2471
|
+
description: request.question,
|
|
2472
|
+
dimensions: dqlArtifact.dimensions ?? [],
|
|
2473
|
+
outputs: [...(dqlArtifact.dimensions ?? []), ...(dqlArtifact.metrics ?? [])],
|
|
2474
|
+
certifierVerdict: {
|
|
2475
|
+
ready: true,
|
|
2476
|
+
blocking: [],
|
|
2477
|
+
warnings: [
|
|
2478
|
+
'Ownership is intentionally absent from the AI draft and is assigned by a human during promotion.',
|
|
2479
|
+
'Review the generated DQL, compiled SQL, grain, and result before promotion.',
|
|
2480
|
+
],
|
|
2481
|
+
},
|
|
2482
|
+
};
|
|
2454
2483
|
return {
|
|
2455
|
-
summary:
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2484
|
+
summary: 'Prepared an ownerless, review-required DQL block draft. No file was changed.',
|
|
2485
|
+
answer: governedResult.answer,
|
|
2486
|
+
status: 'needs_review',
|
|
2487
|
+
trustState: 'review_required',
|
|
2488
|
+
stopReason: 'human_review_required',
|
|
2489
|
+
artifacts: [agentRunArtifact('dql_block_draft', result.name, result, undefined, 'review_required')],
|
|
2459
2490
|
evaluations: [
|
|
2460
|
-
|
|
2461
|
-
agentRunEvaluation('
|
|
2462
|
-
? 'The draft has no automatic certifier blockers, but certification still requires human review.'
|
|
2463
|
-
: 'The draft has certifier blockers that must be resolved before certification review.', result),
|
|
2491
|
+
...(governedResult.evaluations ?? []),
|
|
2492
|
+
agentRunEvaluation('block-commit-boundary', 'Explicit Block Studio commit', true, 'warning', 'Generation produced an ownerless transient DQL artifact. Add to Block Studio is the only action that writes the review draft.', result),
|
|
2464
2493
|
],
|
|
2465
2494
|
nextActions: [
|
|
2466
|
-
{ id: '
|
|
2467
|
-
{ id: 'build-app', label: 'Build app from block', route: 'app_build', artifactKind: 'app_draft' },
|
|
2495
|
+
{ id: 'add-to-block-studio', label: 'Add to Block Studio', artifactKind: 'dql_block_draft' },
|
|
2468
2496
|
],
|
|
2469
2497
|
};
|
|
2470
2498
|
},
|
|
@@ -15188,7 +15216,7 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
15188
15216
|
diagnostics.push({
|
|
15189
15217
|
severity: 'error',
|
|
15190
15218
|
code: 'semantic_metric_missing',
|
|
15191
|
-
message: 'Semantic block is missing a metric. Add
|
|
15219
|
+
message: 'Semantic block is missing a metric. Add metrics = ["metric_name"].',
|
|
15192
15220
|
});
|
|
15193
15221
|
return { sql: null, diagnostics, semanticRefs };
|
|
15194
15222
|
}
|
|
@@ -15214,12 +15242,9 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
15214
15242
|
if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
15215
15243
|
return { sql: null, diagnostics, semanticRefs };
|
|
15216
15244
|
}
|
|
15217
|
-
//
|
|
15218
|
-
//
|
|
15219
|
-
//
|
|
15220
|
-
// keeps metric-bound blocks runnable offline (the full MetricFlow engine may be
|
|
15221
|
-
// unavailable). We still validated the metric exists above. Returning the query
|
|
15222
|
-
// as the composed SQL lets the normal {{ ref() }} resolution + execution apply.
|
|
15245
|
+
// Legacy import-adapter compatibility: older semantic blocks may carry a
|
|
15246
|
+
// pre-compiled `query`. Current writers keep compiled SQL outside semantic DQL,
|
|
15247
|
+
// but existing files remain executable while they are migrated by the formatter.
|
|
15223
15248
|
const precompiledQuery = extractBlockStudioSql(source);
|
|
15224
15249
|
if (precompiledQuery) {
|
|
15225
15250
|
return { sql: precompiledQuery, diagnostics, semanticRefs };
|
|
@@ -15903,6 +15928,95 @@ function saveDqlGenerationDraftForProject(projectRoot, importId, candidate) {
|
|
|
15903
15928
|
export function sanitizeAgentBlockDraftSource(source) {
|
|
15904
15929
|
return source.replace(/^\s*owner\s*=\s*.+(?:\r?\n|$)/gmi, '');
|
|
15905
15930
|
}
|
|
15931
|
+
/**
|
|
15932
|
+
* UI-016 / CONTRACT-002 / AGT-021 — derive the transient Block AI artifact
|
|
15933
|
+
* from the exact governed answer artifact. Semantic drafts are re-rendered
|
|
15934
|
+
* through the one canonical metrics/dimensions writer; custom SQL drafts retain
|
|
15935
|
+
* their query body. Both become ownerless, review-required drafts and never
|
|
15936
|
+
* inherit a saved/certified identity from their answer source.
|
|
15937
|
+
*/
|
|
15938
|
+
export function ownerlessReviewDqlArtifactFromAnswer(answer, question) {
|
|
15939
|
+
const artifact = normalizeDqlArtifactReference(answer.result?.dqlArtifact)
|
|
15940
|
+
?? normalizeDqlArtifactReference(answer.dqlArtifact);
|
|
15941
|
+
if (!artifact?.source)
|
|
15942
|
+
return undefined;
|
|
15943
|
+
const parsed = parseSemanticBlockConfig(artifact.source);
|
|
15944
|
+
const metrics = Array.from(new Set([
|
|
15945
|
+
...(artifact.metrics ?? []),
|
|
15946
|
+
...parsed.metrics,
|
|
15947
|
+
...(parsed.metric ? [parsed.metric] : []),
|
|
15948
|
+
].map((metric) => metric.trim()).filter(Boolean)));
|
|
15949
|
+
const dimensions = Array.from(new Set([
|
|
15950
|
+
...(artifact.dimensions ?? []),
|
|
15951
|
+
...parsed.dimensions,
|
|
15952
|
+
].map((dimension) => dimension.trim()).filter(Boolean)));
|
|
15953
|
+
const name = artifact.name
|
|
15954
|
+
?? artifact.source.match(/\bblock\s+"([^"]+)"/i)?.[1]
|
|
15955
|
+
?? deriveGeneratedDraftSlug(question);
|
|
15956
|
+
const compiledSql = artifact.compiledSql
|
|
15957
|
+
?? answer.result?.sql
|
|
15958
|
+
?? answer.proposedSql
|
|
15959
|
+
?? answer.sql;
|
|
15960
|
+
if (artifact.kind === 'semantic_block' || parsed.blockType === 'semantic' || metrics.length > 0) {
|
|
15961
|
+
const source = renderSemanticBlockSource({
|
|
15962
|
+
name,
|
|
15963
|
+
status: 'draft',
|
|
15964
|
+
domain: parseBlockStudioStringField(artifact.source, 'domain'),
|
|
15965
|
+
description: question,
|
|
15966
|
+
tags: ['ai-generated', 'review-required'],
|
|
15967
|
+
metrics,
|
|
15968
|
+
dimensions,
|
|
15969
|
+
timeDimension: artifact.timeDimension ?? (parsed.timeDimension && parsed.granularity
|
|
15970
|
+
? { name: parsed.timeDimension, granularity: parsed.granularity }
|
|
15971
|
+
: undefined),
|
|
15972
|
+
requestedFilters: (artifact.filters ?? parsed.filters).flatMap((filter) => filter.values.length > 0
|
|
15973
|
+
? filter.values.map((value) => `${filter.dimension}=${value}`)
|
|
15974
|
+
: [filter.dimension]),
|
|
15975
|
+
orderBy: artifact.orderBy,
|
|
15976
|
+
limit: artifact.limit ?? parsed.limit,
|
|
15977
|
+
parameters: artifact.parameters?.map((parameter) => ({
|
|
15978
|
+
name: parameter.name,
|
|
15979
|
+
type: parameter.type,
|
|
15980
|
+
default: parameter.default,
|
|
15981
|
+
policy: parameter.policy,
|
|
15982
|
+
binding: parameter.binding?.kind === 'semantic_filter'
|
|
15983
|
+
? parameter.binding.field
|
|
15984
|
+
: parameter.binding?.kind === 'limit'
|
|
15985
|
+
? 'limit'
|
|
15986
|
+
: undefined,
|
|
15987
|
+
})),
|
|
15988
|
+
visualization: metrics.length > 1 ? { chart: 'table' } : undefined,
|
|
15989
|
+
});
|
|
15990
|
+
return {
|
|
15991
|
+
...artifact,
|
|
15992
|
+
kind: 'semantic_block',
|
|
15993
|
+
name,
|
|
15994
|
+
source,
|
|
15995
|
+
sourcePath: undefined,
|
|
15996
|
+
metrics,
|
|
15997
|
+
dimensions,
|
|
15998
|
+
persistence: 'transient',
|
|
15999
|
+
trustState: 'review_required',
|
|
16000
|
+
...(compiledSql ? { compiledSql } : {}),
|
|
16001
|
+
};
|
|
16002
|
+
}
|
|
16003
|
+
let source = sanitizeAgentBlockDraftSource(artifact.source);
|
|
16004
|
+
if (/\bstatus\s*=\s*"[^"]*"/i.test(source)) {
|
|
16005
|
+
source = source.replace(/\bstatus\s*=\s*"[^"]*"/i, 'status = "draft"');
|
|
16006
|
+
}
|
|
16007
|
+
else {
|
|
16008
|
+
source = source.replace(/(\bblock\s+"[^"]+"\s*\{)/i, '$1\n status = "draft"');
|
|
16009
|
+
}
|
|
16010
|
+
return {
|
|
16011
|
+
...artifact,
|
|
16012
|
+
name,
|
|
16013
|
+
source,
|
|
16014
|
+
sourcePath: undefined,
|
|
16015
|
+
persistence: 'transient',
|
|
16016
|
+
trustState: 'review_required',
|
|
16017
|
+
...(compiledSql ? { compiledSql } : {}),
|
|
16018
|
+
};
|
|
16019
|
+
}
|
|
15906
16020
|
export function saveBlockStudioArtifacts(projectRoot, options) {
|
|
15907
16021
|
const slug = options.name.toLowerCase().replace(/[^a-z0-9_]+/g, '-').replace(/^[-_]+|[-_]+$/g, '') || 'block';
|
|
15908
16022
|
const safeDomain = (options.domain ?? '')
|