@skeletiq/mcp 0.1.1 → 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.
- package/README.md +1 -1
- package/dist/index.js +171 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ change your provider keys, see billing, or delete your account, whatever scopes
|
|
|
85
85
|
| Tool | What it does |
|
|
86
86
|
|---|---|
|
|
87
87
|
| `list_projects` | Find a project by name. Returns the candidates rather than guessing between them. |
|
|
88
|
-
| `get_design` | Read a design, in one of six modes: `overview`, `component`, `brief`, `readiness`, `build_order`, `gaps`. |
|
|
88
|
+
| `get_design` | Read a design, in one of six modes: `overview`, `component`, `brief`, `readiness`, `build_order`, `gaps`. `overview` and `component` resolve the design's `R`-ids to the requirements they name, and what became of each. |
|
|
89
89
|
| `generate_architecture` | Design a system from a prompt. Spends credits and takes minutes. |
|
|
90
90
|
| `get_generation_status` | Poll a generation started with `wait: false`. |
|
|
91
91
|
| `critique_architecture` | Check a design against SkeletIQ's rules. Deterministic, free, stores nothing. Tell it the `domain` and the `exposure`. |
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
var SERVER_NAME = "skeletiq";
|
|
8
|
-
var SERVER_VERSION = "0.
|
|
8
|
+
var SERVER_VERSION = "0.2.0";
|
|
9
9
|
var DEFAULT_API_URL = "https://api.skeletiq.com";
|
|
10
10
|
var TOKEN_PREFIX = "skq_";
|
|
11
11
|
var ConfigError = class extends Error {
|
|
@@ -322,6 +322,40 @@ var ArchitectureJsonSchema = z.looseObject({
|
|
|
322
322
|
assumptions: z.array(GapEntrySchema).nullable().optional(),
|
|
323
323
|
open_questions: z.array(GapEntrySchema).nullable().optional()
|
|
324
324
|
});
|
|
325
|
+
var RequirementLedgerRowSchema = z.looseObject({
|
|
326
|
+
id: z.string(),
|
|
327
|
+
text: z.string().optional(),
|
|
328
|
+
kind: z.string().optional(),
|
|
329
|
+
source: z.string().optional(),
|
|
330
|
+
/** `unmet` | `uncited` | `unchecked` | `cited` — deliberately never `met`. Left as a string: a
|
|
331
|
+
* value added server-side must not stop an older connector parsing the row. */
|
|
332
|
+
status: z.string().optional(),
|
|
333
|
+
verifier_checked: z.boolean().optional(),
|
|
334
|
+
cited_by_components: z.array(z.string()).nullable().optional(),
|
|
335
|
+
/** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
|
|
336
|
+
cited_by_decisions: z.array(z.number()).nullable().optional(),
|
|
337
|
+
added_in_version: z.number().nullable().optional()
|
|
338
|
+
});
|
|
339
|
+
var RequirementLedgerCountsSchema = z.looseObject({
|
|
340
|
+
total: z.number().optional(),
|
|
341
|
+
user_supplied: z.number().optional(),
|
|
342
|
+
llm_inferred: z.number().optional(),
|
|
343
|
+
unmet: z.number().optional(),
|
|
344
|
+
uncited: z.number().optional(),
|
|
345
|
+
unchecked: z.number().optional(),
|
|
346
|
+
cited: z.number().optional()
|
|
347
|
+
});
|
|
348
|
+
var RequirementLedgerSchema = z.looseObject({
|
|
349
|
+
rows: z.array(RequirementLedgerRowSchema).nullable().optional(),
|
|
350
|
+
analysis_version: z.number().nullable().optional(),
|
|
351
|
+
analysis_degraded: z.boolean().optional(),
|
|
352
|
+
/** `grounded` | `not_grounded` | `diverged`. */
|
|
353
|
+
grounding_state: z.string().optional(),
|
|
354
|
+
/** `ran` | `partial` | `not_run`. */
|
|
355
|
+
verification_state: z.string().optional(),
|
|
356
|
+
verified_version: z.number().nullable().optional(),
|
|
357
|
+
counts: RequirementLedgerCountsSchema.nullable().optional()
|
|
358
|
+
});
|
|
325
359
|
var ArchitectureSchema = z.looseObject({
|
|
326
360
|
id: z.string(),
|
|
327
361
|
project_id: z.string(),
|
|
@@ -332,7 +366,8 @@ var ArchitectureSchema = z.looseObject({
|
|
|
332
366
|
created_at: z.string().optional(),
|
|
333
367
|
updated_at: z.string().optional(),
|
|
334
368
|
is_released: z.boolean().optional(),
|
|
335
|
-
released_at: z.string().nullable().optional()
|
|
369
|
+
released_at: z.string().nullable().optional(),
|
|
370
|
+
requirement_ledger: RequirementLedgerSchema.nullable().optional()
|
|
336
371
|
});
|
|
337
372
|
var ArchitecturePageSchema = z.looseObject({
|
|
338
373
|
items: z.array(ArchitectureSchema),
|
|
@@ -1037,7 +1072,14 @@ function frameDetail(frame) {
|
|
|
1037
1072
|
var constraintsInput = z4.strictObject({
|
|
1038
1073
|
budget: z4.enum(["low", "medium", "high", "unlimited"]).optional(),
|
|
1039
1074
|
max_latency_ms: z4.number().int().positive().optional(),
|
|
1040
|
-
|
|
1075
|
+
// Free strings, not an enum, and the description has to say why. SkeletIQ assesses five
|
|
1076
|
+
// frameworks; naming anything else is still worth doing, because an unassessed regime is
|
|
1077
|
+
// recorded and reaches the design brief as a requirement the agent still has to satisfy.
|
|
1078
|
+
// The old text read 'e.g. HIPAA, SOC2, PCI-DSS, GDPR' — it omitted SOX, which is the one
|
|
1079
|
+
// an agent is least likely to guess, and said nothing about what happens to the rest.
|
|
1080
|
+
compliance: z4.array(z4.string()).optional().describe(
|
|
1081
|
+
"Regimes the design must satisfy. SOC2, HIPAA, PCI-DSS, GDPR and SOX are checked against requirement catalogues; any other regime is recorded and carried into the design brief as a requirement to satisfy, rather than checked."
|
|
1082
|
+
),
|
|
1041
1083
|
preferred_cloud: z4.enum(["aws", "gcp", "azure", "any"]).optional(),
|
|
1042
1084
|
preferred_technologies: z4.array(z4.string()).optional(),
|
|
1043
1085
|
max_components: z4.number().int().positive().optional(),
|
|
@@ -1362,32 +1404,63 @@ async function describeCreated(resolver, architectureId) {
|
|
|
1362
1404
|
import * as z5 from "zod/v4";
|
|
1363
1405
|
|
|
1364
1406
|
// src/lib/slice.ts
|
|
1365
|
-
function sliceComponent(design, componentId) {
|
|
1407
|
+
function sliceComponent(design, componentId, ledger) {
|
|
1366
1408
|
const components = design.components ?? [];
|
|
1367
1409
|
const component = components.find((c) => c.id === componentId) ?? components.find((c) => normalize(c.name) === normalize(componentId));
|
|
1368
1410
|
if (!component) return null;
|
|
1369
1411
|
const connections = design.connections ?? [];
|
|
1412
|
+
const related = relatedDecisions(design, component);
|
|
1370
1413
|
return {
|
|
1371
1414
|
component,
|
|
1372
1415
|
outgoing: connections.filter((c) => c.source === component.id),
|
|
1373
1416
|
incoming: connections.filter((c) => c.target === component.id),
|
|
1374
|
-
related_decisions:
|
|
1417
|
+
related_decisions: related,
|
|
1418
|
+
assumed_decisions: assumedAmong(design, related),
|
|
1419
|
+
...ledger ? { cited_requirements: citedRequirements(ledger, component.id) } : {}
|
|
1375
1420
|
};
|
|
1376
1421
|
}
|
|
1422
|
+
function citedRequirements(ledger, componentId) {
|
|
1423
|
+
return orderRequirementRows(
|
|
1424
|
+
(ledger.rows ?? []).filter((row) => (row.cited_by_components ?? []).includes(componentId))
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
var STATUS_ORDER = ["unmet", "uncited", "unchecked", "cited"];
|
|
1428
|
+
function orderRequirementRows(rows) {
|
|
1429
|
+
return [...rows].sort((a, b) => statusRank(a) - statusRank(b));
|
|
1430
|
+
}
|
|
1431
|
+
function statusRank(row) {
|
|
1432
|
+
const index = STATUS_ORDER.indexOf(row.status ?? "");
|
|
1433
|
+
return index === -1 ? STATUS_ORDER.length : index;
|
|
1434
|
+
}
|
|
1377
1435
|
function decisionTexts(design) {
|
|
1378
|
-
|
|
1436
|
+
return decisionEntries(design).map((entry) => entry.text);
|
|
1437
|
+
}
|
|
1438
|
+
function decisionEntries(design) {
|
|
1439
|
+
const byText = /* @__PURE__ */ new Map();
|
|
1379
1440
|
const out = [];
|
|
1380
|
-
const push2 = (text) => {
|
|
1441
|
+
const push2 = (text, assumption) => {
|
|
1381
1442
|
if (typeof text !== "string") return;
|
|
1382
1443
|
const trimmed = text.trim();
|
|
1383
|
-
if (trimmed.length === 0
|
|
1384
|
-
|
|
1385
|
-
|
|
1444
|
+
if (trimmed.length === 0) return;
|
|
1445
|
+
const existing = byText.get(trimmed);
|
|
1446
|
+
if (existing) {
|
|
1447
|
+
existing.assumption = existing.assumption || assumption;
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const entry = { text: trimmed, assumption };
|
|
1451
|
+
byText.set(trimmed, entry);
|
|
1452
|
+
out.push(entry);
|
|
1386
1453
|
};
|
|
1387
|
-
for (const decision of design.design_decisions ?? []) push2(decision);
|
|
1388
|
-
for (const decision of design.grounded_decisions ?? []) push2(decision?.text);
|
|
1454
|
+
for (const decision of design.design_decisions ?? []) push2(decision, false);
|
|
1455
|
+
for (const decision of design.grounded_decisions ?? []) push2(decision?.text, decision?.assumption === true);
|
|
1389
1456
|
return out;
|
|
1390
1457
|
}
|
|
1458
|
+
function assumedAmong(design, texts) {
|
|
1459
|
+
const assumed = new Set(
|
|
1460
|
+
decisionEntries(design).filter((entry) => entry.assumption).map((entry) => entry.text)
|
|
1461
|
+
);
|
|
1462
|
+
return texts.filter((text) => assumed.has(text));
|
|
1463
|
+
}
|
|
1391
1464
|
function relatedDecisions(design, component) {
|
|
1392
1465
|
const needles = [component.name, component.technology].filter((value) => typeof value === "string" && value.trim().length > 2).map((value) => value.toLowerCase());
|
|
1393
1466
|
if (needles.length === 0) return [];
|
|
@@ -1396,14 +1469,24 @@ function relatedDecisions(design, component) {
|
|
|
1396
1469
|
return needles.some((needle) => haystack.includes(needle));
|
|
1397
1470
|
});
|
|
1398
1471
|
}
|
|
1399
|
-
function overview(design) {
|
|
1472
|
+
function overview(design, ledger) {
|
|
1473
|
+
const decisions = decisionTexts(design);
|
|
1400
1474
|
return {
|
|
1401
1475
|
title: design.title ?? "Untitled design",
|
|
1402
1476
|
description: design.description ?? "",
|
|
1403
1477
|
components: design.components ?? [],
|
|
1404
1478
|
connections: design.connections ?? [],
|
|
1405
|
-
design_decisions:
|
|
1406
|
-
|
|
1479
|
+
design_decisions: decisions,
|
|
1480
|
+
// A subset of `design_decisions`, not a second list of decisions: which of them the design
|
|
1481
|
+
// marks as resting on no cited source. Additive, so `design_decisions` stays the plain
|
|
1482
|
+
// strings every existing caller reads.
|
|
1483
|
+
assumed_decisions: assumedAmong(design, decisions),
|
|
1484
|
+
trade_offs: design.trade_offs ?? [],
|
|
1485
|
+
// Carried verbatim, rows in the server's order, and only when the version has one. This is
|
|
1486
|
+
// a rebuild from named keys, so anything not named here is dropped without a trace — which
|
|
1487
|
+
// is how `grounded_decisions` went missing for two releases. Re-sorting the rows on the way
|
|
1488
|
+
// past would make "verbatim" untrue; the grouping belongs to the text that prints them.
|
|
1489
|
+
...ledger ? { requirement_ledger: ledger } : {}
|
|
1407
1490
|
};
|
|
1408
1491
|
}
|
|
1409
1492
|
function normalize(value) {
|
|
@@ -1415,7 +1498,7 @@ var MODES = ["overview", "component", "brief", "readiness", "build_order", "gaps
|
|
|
1415
1498
|
var input3 = z5.object({
|
|
1416
1499
|
project_id: z5.string().describe("A SkeletIQ project id, or its exact name."),
|
|
1417
1500
|
mode: z5.enum(MODES).describe(
|
|
1418
|
-
"overview: the whole design. component: one component, its connections in both directions,
|
|
1501
|
+
"overview: the whole design, and the requirements it was built from with what became of each. component: one component, its connections in both directions, the decisions that mention it, and the requirements the design cites it for. brief: the fenced markdown block to write into AGENTS.md. readiness: what still has to be decided before this design is worth building. build_order: the order to build components in, and why. gaps: the open questions and unconfirmed assumptions in the design."
|
|
1419
1502
|
),
|
|
1420
1503
|
component_id: z5.string().optional().describe('Required for mode "component". The component id, or its name.'),
|
|
1421
1504
|
version: z5.number().int().positive().optional().describe("A specific version. Defaults to the latest released version, or the latest version if none is released.")
|
|
@@ -1431,12 +1514,19 @@ var output3 = z5.object({
|
|
|
1431
1514
|
/** Mode-shaped. Deliberately loose: six modes cannot share one strict schema honestly. */
|
|
1432
1515
|
data: z5.unknown()
|
|
1433
1516
|
});
|
|
1517
|
+
function decisionsHeading(assumed, settled, withAssumptions) {
|
|
1518
|
+
return assumed.length > 0 ? withAssumptions : settled;
|
|
1519
|
+
}
|
|
1520
|
+
function decisionLines(decisions, assumed) {
|
|
1521
|
+
const marked = new Set(assumed);
|
|
1522
|
+
return decisions.map((decision) => `- ${decision}${marked.has(decision) ? " (assumption)" : ""}`);
|
|
1523
|
+
}
|
|
1434
1524
|
function registerGetDesign(server, client, resolver) {
|
|
1435
1525
|
server.registerTool(
|
|
1436
1526
|
"get_design",
|
|
1437
1527
|
{
|
|
1438
1528
|
title: "Read a SkeletIQ design",
|
|
1439
|
-
description: 'Read a SkeletIQ architecture. Start with mode "brief" to orient \u2014 it returns a fenced block to write into AGENTS.md \u2014 then "build_order" for the sequence and "component" for each piece as you build it. Every answer states which version it came from and whether that version is a release.',
|
|
1529
|
+
description: 'Read a SkeletIQ architecture. Start with mode "brief" to orient \u2014 it returns a fenced block to write into AGENTS.md \u2014 then "build_order" for the sequence and "component" for each piece as you build it. "overview" and "component" also resolve the R-ids the design carries to the requirements they name, and say what became of each. Every answer states which version it came from and whether that version is a release.',
|
|
1440
1530
|
inputSchema: input3,
|
|
1441
1531
|
outputSchema: output3,
|
|
1442
1532
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
@@ -1452,7 +1542,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1452
1542
|
switch (mode) {
|
|
1453
1543
|
case "overview": {
|
|
1454
1544
|
const design = await resolver.design(resolved.architectureId);
|
|
1455
|
-
const data = overview(design.architecture_json);
|
|
1545
|
+
const data = overview(design.architecture_json, design.requirement_ledger);
|
|
1456
1546
|
const facts = factsFrom(resolved);
|
|
1457
1547
|
return ok({ ...base, ...facts, data }, [
|
|
1458
1548
|
`# ${data.title}`,
|
|
@@ -1462,6 +1552,10 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1462
1552
|
...data.components.map(
|
|
1463
1553
|
(c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
|
|
1464
1554
|
),
|
|
1555
|
+
// Above the decisions because the two answer different questions, in
|
|
1556
|
+
// this order: requirements are what was asked for, decisions are how
|
|
1557
|
+
// the design answered.
|
|
1558
|
+
...requirementBlock(data.requirement_ledger),
|
|
1465
1559
|
// The design's own reasoning, which this text used to omit entirely
|
|
1466
1560
|
// while carrying it in the structured half — so an agent working from
|
|
1467
1561
|
// what it was shown rebuilt every choice from scratch, and could
|
|
@@ -1469,8 +1563,12 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1469
1563
|
// like this", and the overview is where that gets asked.
|
|
1470
1564
|
...data.design_decisions.length ? [
|
|
1471
1565
|
"",
|
|
1472
|
-
|
|
1473
|
-
|
|
1566
|
+
decisionsHeading(
|
|
1567
|
+
data.assumed_decisions,
|
|
1568
|
+
"Decisions taken \u2014 these are settled, build to them:",
|
|
1569
|
+
"Decisions taken \u2014 build to them; confirm the ones marked (assumption) first, they rest on no cited source:"
|
|
1570
|
+
),
|
|
1571
|
+
...decisionLines(data.design_decisions, data.assumed_decisions)
|
|
1474
1572
|
] : [],
|
|
1475
1573
|
...data.trade_offs.length ? [
|
|
1476
1574
|
"",
|
|
@@ -1486,7 +1584,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1486
1584
|
return failure('mode "component" needs a component_id. Use mode "overview" or "build_order" to see the ids.');
|
|
1487
1585
|
}
|
|
1488
1586
|
const design = await resolver.design(resolved.architectureId);
|
|
1489
|
-
const slice = sliceComponent(design.architecture_json, component_id);
|
|
1587
|
+
const slice = sliceComponent(design.architecture_json, component_id, design.requirement_ledger);
|
|
1490
1588
|
if (!slice) {
|
|
1491
1589
|
const known = (design.architecture_json.components ?? []).map((c) => c.id).join(", ");
|
|
1492
1590
|
return failure(
|
|
@@ -1502,6 +1600,9 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1502
1600
|
"",
|
|
1503
1601
|
outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
|
|
1504
1602
|
incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
|
|
1603
|
+
// Same order as the overview: what was asked for, then how the design
|
|
1604
|
+
// answered.
|
|
1605
|
+
...requirementsServed(slice.cited_requirements),
|
|
1505
1606
|
// Labelled, and only when there are any. These bullets used to be
|
|
1506
1607
|
// printed bare under "Called by:", so a decision about the component
|
|
1507
1608
|
// read as a continuation of its edge list — and a design with no
|
|
@@ -1509,8 +1610,12 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1509
1610
|
// with no reasoning rather than as a decision that names other boxes.
|
|
1510
1611
|
...slice.related_decisions.length ? [
|
|
1511
1612
|
"",
|
|
1512
|
-
|
|
1513
|
-
|
|
1613
|
+
decisionsHeading(
|
|
1614
|
+
slice.assumed_decisions,
|
|
1615
|
+
"Decisions that mention it:",
|
|
1616
|
+
"Decisions that mention it \u2014 confirm the ones marked (assumption) first, they rest on no cited source:"
|
|
1617
|
+
),
|
|
1618
|
+
...decisionLines(slice.related_decisions, slice.assumed_decisions)
|
|
1514
1619
|
] : [
|
|
1515
1620
|
"",
|
|
1516
1621
|
"No decision in this design names this component. That is not the same as",
|
|
@@ -1637,6 +1742,49 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1637
1742
|
})
|
|
1638
1743
|
);
|
|
1639
1744
|
}
|
|
1745
|
+
function requirementBlock(ledger) {
|
|
1746
|
+
const rows = orderRequirementRows(ledger?.rows ?? []);
|
|
1747
|
+
if (!ledger || rows.length === 0) return [];
|
|
1748
|
+
const attention = ["unmet", "uncited", "unchecked"].map((status) => [rows.filter((row) => row.status === status).length, status]).filter(([count]) => count > 0).map(([count, status]) => `${count} ${status}`);
|
|
1749
|
+
return [
|
|
1750
|
+
"",
|
|
1751
|
+
`Requirements \u2014 ${rows.length} total${attention.length ? `; ${attention.join(", ")}` : ""}:`,
|
|
1752
|
+
...rows.map(requirementLine),
|
|
1753
|
+
"Statuses: `unmet` \u2014 the verifier judged the design does not satisfy it; `uncited` \u2014 nothing",
|
|
1754
|
+
"in the design claims to address it; `unchecked` \u2014 something cites it and the verifier did not",
|
|
1755
|
+
"check it; `cited` \u2014 cited with no gap found, which is not the same as proven.",
|
|
1756
|
+
// Two facts about how far the statuses can be trusted, said only when the server states
|
|
1757
|
+
// them. An absent `verification_state` is an older server, not a verifier that did not run,
|
|
1758
|
+
// and claiming the second would be this package inventing a fact about someone's design.
|
|
1759
|
+
...ledger.grounding_state === "not_grounded" ? [
|
|
1760
|
+
"This version records no citations at all, so `uncited` here means nothing recorded what",
|
|
1761
|
+
"addresses the requirement \u2014 not that nothing in the design does."
|
|
1762
|
+
] : [],
|
|
1763
|
+
...ledger.verification_state === "partial" ? [
|
|
1764
|
+
"The verifier did not check every requirement in this set, so an `unchecked` row is",
|
|
1765
|
+
"neither confirmed nor flagged."
|
|
1766
|
+
] : ledger.verification_state === "not_run" ? [
|
|
1767
|
+
"The verifier did not run on this version, so an `unchecked` row is neither confirmed",
|
|
1768
|
+
"nor flagged."
|
|
1769
|
+
] : [],
|
|
1770
|
+
...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
|
|
1771
|
+
];
|
|
1772
|
+
}
|
|
1773
|
+
function requirementsServed(rows) {
|
|
1774
|
+
if (!rows?.length) return [];
|
|
1775
|
+
return [
|
|
1776
|
+
"",
|
|
1777
|
+
"Requirements the design cites this component for:",
|
|
1778
|
+
...rows.map(requirementLine),
|
|
1779
|
+
"`unmet` \u2014 the verifier judged the design does not satisfy it; `unchecked` \u2014 the verifier did",
|
|
1780
|
+
"not check it; `cited` \u2014 no gap found, which is not the same as proven."
|
|
1781
|
+
];
|
|
1782
|
+
}
|
|
1783
|
+
function requirementLine(row) {
|
|
1784
|
+
const status = row.status ? ` [${row.status}]` : "";
|
|
1785
|
+
const text = (row.text ?? "").replace(/\s+/g, " ").trim();
|
|
1786
|
+
return `- ${row.id}${status} ${text || "(no text recorded for this requirement)"}`;
|
|
1787
|
+
}
|
|
1640
1788
|
function settledAnswer(gap) {
|
|
1641
1789
|
const note = (gap.note ?? "").trim();
|
|
1642
1790
|
if (gap.adr_id) {
|
package/package.json
CHANGED