@skeletiq/mcp 0.3.0 → 0.4.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 +4 -3
- package/dist/index.js +206 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ change your provider keys, see billing, or delete your account, whatever scopes
|
|
|
86
86
|
|---|---|
|
|
87
87
|
| `list_projects` | Find a project by name. Returns the candidates rather than guessing between them. |
|
|
88
88
|
| `get_design` | Read a design, in one of six modes: `overview`, `component`, `brief`, `readiness`, `build_order`, `gaps`. Where a version records its requirements, `overview` and `component` resolve the design's `R`-ids to the requirements they name, and say what became of each. |
|
|
89
|
-
| `generate_architecture` | Design a system from a prompt. Spends credits and takes minutes. |
|
|
89
|
+
| `generate_architecture` | Design a system from a prompt. Spends credits and takes minutes. A prompt that states no load comes back with a question instead, and nothing is charged: ask your human, and answer "Not sure yet" when they do not know rather than guessing. |
|
|
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`. |
|
|
92
92
|
| `check_drift` | Report what you built; hear what is missing, half done, or not in the design. |
|
|
@@ -114,8 +114,9 @@ unknown ids with suggestions, they are suggestions — put them to a person rath
|
|
|
114
114
|
mapping.
|
|
115
115
|
|
|
116
116
|
**`critique_architecture`'s optional inputs are not neutral.** Omitting one does not skip a
|
|
117
|
-
question; it answers it. With no `domain
|
|
118
|
-
|
|
117
|
+
question; it answers it. With no `domain`, no compliance framework applies (`secondary_domains`
|
|
118
|
+
select none — they add that domain's design checks as advice — so name a framework when it must
|
|
119
|
+
apply), so no compliance finding is possible and the score comes back higher than the SkeletIQ app shows
|
|
119
120
|
for the same design — by up to 15 points. With no `exposure`, the design is assessed as
|
|
120
121
|
internet-facing, which is how an air-gapped system gets told to add a CDN and a WAF. The response
|
|
121
122
|
says what was actually used — `frameworks_checked` and `exposure_assessed` — and the text output
|
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.4.0";
|
|
9
9
|
var DEFAULT_API_URL = "https://api.skeletiq.com";
|
|
10
10
|
var TOKEN_PREFIX = "skq_";
|
|
11
11
|
var ConfigError = class extends Error {
|
|
@@ -60,12 +60,12 @@ var SkeletiqApiError = class extends Error {
|
|
|
60
60
|
/**
|
|
61
61
|
* A `GET` may be retried after a 429; a `POST` that may have spent credits may not.
|
|
62
62
|
*
|
|
63
|
-
* A spent daily allowance is the
|
|
64
|
-
* measured in hours, so a caller backing off and trying
|
|
65
|
-
* call that cannot succeed today.
|
|
63
|
+
* A spent daily allowance is the 429 that is *not* retryable — the account's, or a runtime's
|
|
64
|
+
* shared one: each refills on a clock measured in hours, so a caller backing off and trying
|
|
65
|
+
* again is burning its budget on a call that cannot succeed today.
|
|
66
66
|
*/
|
|
67
67
|
get isRetryable() {
|
|
68
|
-
if (this.code === "DAILY_LIMIT_REACHED") return false;
|
|
68
|
+
if (this.code === "DAILY_LIMIT_REACHED" || this.code === "RUNTIME_ALLOWANCE_SPENT") return false;
|
|
69
69
|
return this.status === 429 || this.code === "IDEMPOTENCY_UNAVAILABLE";
|
|
70
70
|
}
|
|
71
71
|
};
|
|
@@ -138,6 +138,12 @@ function describeApiError(error) {
|
|
|
138
138
|
if (upgradeUrl) lines.push(`A larger plan raises the daily limit: ${upgradeUrl}`);
|
|
139
139
|
break;
|
|
140
140
|
}
|
|
141
|
+
case "RUNTIME_ALLOWANCE_SPENT": {
|
|
142
|
+
lines.push(error.message);
|
|
143
|
+
const resetAt = asString(detail?.reset_at);
|
|
144
|
+
if (resetAt) lines.push(`The allowance resets at ${resetAt}.`);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
141
147
|
case "PLAN_LIMIT_EXCEEDED":
|
|
142
148
|
case "FEATURE_NOT_AVAILABLE":
|
|
143
149
|
case "VERSION_LOCKED": {
|
|
@@ -344,6 +350,20 @@ var RequirementLedgerRowSchema = z.looseObject({
|
|
|
344
350
|
/** The model behind a `verifier` verdict. Null on every deterministic row — naming a model
|
|
345
351
|
* beside a number the server computed would be a false attribution. */
|
|
346
352
|
checked_by: z.string().nullable().optional(),
|
|
353
|
+
/** Whether this row holds a handoff back: a `fails` a measurement reached (capacity, cost, the
|
|
354
|
+
* review's rules). A model check's `fails` does not block. Absent from older APIs, where
|
|
355
|
+
* `requirementBlocksHandoff` falls back to the method. */
|
|
356
|
+
blocks_handoff: z.boolean().optional(),
|
|
357
|
+
/** Why a `fails` does not block: `model_check` — only the model check failed it, nothing
|
|
358
|
+
* measured it; `unstated_input` — a shortfall that turns on an input nobody gave (an unstated
|
|
359
|
+
* split, or an assumed load — told apart by `unstated_input`); `advisory_finding` — only review
|
|
360
|
+
* advice failed it. Null on every other row. A string, for the reason `status` is one. Absent
|
|
361
|
+
* from older APIs. */
|
|
362
|
+
unconfirmed_reason: z.string().nullable().optional(),
|
|
363
|
+
/** True on a review-rules `fails` whose every finding is advice (`advisory_finding`). */
|
|
364
|
+
advisory_only: z.boolean().optional(),
|
|
365
|
+
/** The assumption an `unstated_input` row turns on, as a clause. Absent from older APIs. */
|
|
366
|
+
unstated_input: z.string().nullable().optional(),
|
|
347
367
|
cited_by_components: z.array(z.string()).nullable().optional(),
|
|
348
368
|
/** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
|
|
349
369
|
cited_by_decisions: z.array(z.number()).nullable().optional(),
|
|
@@ -463,7 +483,13 @@ var ReadinessRowSchema = z.looseObject({
|
|
|
463
483
|
state: z.string(),
|
|
464
484
|
category: z.string(),
|
|
465
485
|
gating: z.boolean(),
|
|
466
|
-
detail: z.string().nullable().optional()
|
|
486
|
+
detail: z.string().nullable().optional(),
|
|
487
|
+
/** On a row that could not be answered: what would answer it ("Saving a change runs the
|
|
488
|
+
* check on the new version."). Absent on a row that has an answer. */
|
|
489
|
+
clears: z.string().nullable().optional(),
|
|
490
|
+
/** True when stating the expected load is what would answer this row — `clears` then says
|
|
491
|
+
* how. Optional so a server that predates the field still parses. */
|
|
492
|
+
clears_by_load: z.boolean().optional()
|
|
467
493
|
});
|
|
468
494
|
var ReadinessSchema = z.looseObject({
|
|
469
495
|
architecture_id: z.string(),
|
|
@@ -516,7 +542,15 @@ var DesignGapSchema = z.looseObject({
|
|
|
516
542
|
problem_key: z.string().optional(),
|
|
517
543
|
/** An answer is recorded and this version was not built with it, so iterating the design
|
|
518
544
|
* would change something. False once a generation has been handed the answer. */
|
|
519
|
-
answer_pending: z.boolean().optional()
|
|
545
|
+
answer_pending: z.boolean().optional(),
|
|
546
|
+
/** The read/write mix or peak this assumption states that the design is not sized on. Null when
|
|
547
|
+
* the two agree; absent from older servers. */
|
|
548
|
+
sizing_conflict: z.looseObject({
|
|
549
|
+
dimension: z.string(),
|
|
550
|
+
assumed: z.string(),
|
|
551
|
+
sized: z.string(),
|
|
552
|
+
resize_to: z.string().nullable().optional()
|
|
553
|
+
}).nullable().optional()
|
|
520
554
|
});
|
|
521
555
|
var OrphanedAnswerSchema = z.looseObject({
|
|
522
556
|
gap_id: z.string(),
|
|
@@ -612,7 +646,10 @@ var TodoItemSchema = z.looseObject({
|
|
|
612
646
|
* it goes back further than the server looked — "an earlier version", never version zero. */
|
|
613
647
|
carried_from_version: z.number().nullable().optional(),
|
|
614
648
|
dismissed: z.boolean().optional(),
|
|
615
|
-
dismissed_reason: z.string().nullable().optional()
|
|
649
|
+
dismissed_reason: z.string().nullable().optional(),
|
|
650
|
+
/** The key of the one readiness row this card is counted on, so the readiness counts can be
|
|
651
|
+
* traced to cards. Null on a card no row counts. */
|
|
652
|
+
counts_toward: z.string().nullable().optional()
|
|
616
653
|
});
|
|
617
654
|
var TodoGroupSchema = z.looseObject({
|
|
618
655
|
key: z.string(),
|
|
@@ -659,13 +696,26 @@ var DriftSchema = z.looseObject({
|
|
|
659
696
|
counts: z.record(z.string(), z.number()),
|
|
660
697
|
release: ReleaseFactsSchema
|
|
661
698
|
});
|
|
699
|
+
var ClarifyingOptionDetailSchema = z.looseObject({
|
|
700
|
+
value: z.string(),
|
|
701
|
+
label: z.string(),
|
|
702
|
+
detail: z.string().optional()
|
|
703
|
+
});
|
|
662
704
|
var ClarifyingQuestionSchema = z.looseObject({
|
|
663
705
|
id: z.string(),
|
|
664
706
|
question: z.string(),
|
|
665
707
|
why: z.string().optional(),
|
|
666
708
|
kind: z.string().optional(),
|
|
667
709
|
options: z.array(z.string()).nullable().optional(),
|
|
668
|
-
decline_options: z.array(z.string()).nullable().optional()
|
|
710
|
+
decline_options: z.array(z.string()).nullable().optional(),
|
|
711
|
+
/** Options that withdraw the instruction to be rewritten, not answer it ("I'll name the parts"). */
|
|
712
|
+
dismiss_options: z.array(z.string()).nullable().optional(),
|
|
713
|
+
/** Each option's label and what it means, keyed by the value in `options`. */
|
|
714
|
+
option_details: z.array(ClarifyingOptionDetailSchema).nullable().optional(),
|
|
715
|
+
/** Whether more than one option may be picked at once. */
|
|
716
|
+
multiple: z.boolean().optional(),
|
|
717
|
+
/** What leaving the question unanswered does, in the product's words. */
|
|
718
|
+
skip_effect: z.string().nullable().optional()
|
|
669
719
|
});
|
|
670
720
|
var IntentDecisionSchema = z.looseObject({
|
|
671
721
|
intent: z.string().optional(),
|
|
@@ -1025,7 +1075,7 @@ var input2 = z3.object({
|
|
|
1025
1075
|
"The design's domain \u2014 e-commerce, fintech, healthcare, saas, social, iot, gaming, infrastructure, streaming, logistics, ai-ml, search, data-analytics, content-platform, internal-tool. Worth sending: the domain is what selects the compliance frameworks to check against, and without it none apply, so no compliance finding is possible and the score comes back higher than the SkeletIQ app would show for the same design \u2014 by up to 15 points."
|
|
1026
1076
|
),
|
|
1027
1077
|
secondary_domains: z3.array(z3.string()).max(8).optional().describe(
|
|
1028
|
-
"Further domains the design spans, when it spans more than one \u2014 a multi-tenant shop that takes payments is e-commerce plus fintech and saas. These
|
|
1078
|
+
"Further domains the design spans, when it spans more than one \u2014 a multi-tenant shop that takes payments is e-commerce plus fintech and saas. These add the design checks that domain usually needs, as advice; they select no compliance frameworks. Only the primary domain's frameworks and the ones the user named are checked, so name a framework in the prompt when it must apply. frameworks_checked says which were used."
|
|
1029
1079
|
),
|
|
1030
1080
|
exposure: z3.enum(["public_internet", "private_network", "air_gapped"]).optional().describe(
|
|
1031
1081
|
"Where the design runs. Worth sending: four checks \u2014 CDN, WAF, rate limiting and multi-region \u2014 ask whether traffic arriving from the public internet is handled safely, and they only apply to a system that takes any. Left unset, the design is assessed as internet-facing, which is why an air-gapped design comes back told to add a CDN. exposure_assessed says which exposure was actually used."
|
|
@@ -1203,9 +1253,10 @@ var PROGRESS_LABELS = {
|
|
|
1203
1253
|
refining: "Refining the design"
|
|
1204
1254
|
};
|
|
1205
1255
|
var PROGRESS_TOTAL = Object.keys(PROGRESS_LABELS).length + 1;
|
|
1256
|
+
var LOAD_NOT_SURE = "Not sure yet";
|
|
1206
1257
|
var PROVENANCE_LABELS = {
|
|
1207
|
-
user_supplied: "
|
|
1208
|
-
llm_inferred: "
|
|
1258
|
+
user_supplied: "you set this",
|
|
1259
|
+
llm_inferred: "read from your prompt",
|
|
1209
1260
|
tier_default: "assumed",
|
|
1210
1261
|
guardrail_clamped: "adjusted"
|
|
1211
1262
|
};
|
|
@@ -1216,16 +1267,19 @@ function asText(value) {
|
|
|
1216
1267
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1217
1268
|
return typeof value === "string" && value.trim() && value !== "unknown" ? value.trim() : void 0;
|
|
1218
1269
|
}
|
|
1270
|
+
var BARE_FIGURE = /^~?\d[\d,]*(\.\d+)?$/;
|
|
1219
1271
|
function frameDetail(frame) {
|
|
1220
1272
|
const data = asRecord2(frame.pipeline_step);
|
|
1221
1273
|
if (!data) return void 0;
|
|
1222
1274
|
const parts = [];
|
|
1223
1275
|
const domain = asText(data.domain);
|
|
1224
1276
|
if (domain) parts.push(domain);
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
1277
|
+
const rawRps = asText(data.requests_per_second);
|
|
1278
|
+
const zeroRate = rawRps !== void 0 && Number(rawRps.replace(/[~,]/g, "")) === 0;
|
|
1279
|
+
const rps = zeroRate ? void 0 : rawRps;
|
|
1280
|
+
const figure = rps ? BARE_FIGURE.test(rps) ? `${rps}/s` : rps : asText(data.scale);
|
|
1227
1281
|
if (figure) {
|
|
1228
|
-
const source = asText(data.scale_source);
|
|
1282
|
+
const source = zeroRate ? void 0 : asText(data.scale_source);
|
|
1229
1283
|
const label = source ? PROVENANCE_LABELS[source] ?? source : void 0;
|
|
1230
1284
|
parts.push(label ? `${figure} (${label})` : figure);
|
|
1231
1285
|
}
|
|
@@ -1249,11 +1303,17 @@ var constraintsInput = z4.strictObject({
|
|
|
1249
1303
|
"Wins over any share stated in the prompt, which the server also reads. Unset and unstated, the design assumes 80% reads. It sizes components and weights read replicas, so on a read-heavy design it changes the instance counts and the cost."
|
|
1250
1304
|
),
|
|
1251
1305
|
burstiness: z4.enum(["steady", "daily_peaks", "spiky"]).optional().describe(
|
|
1252
|
-
'Wins over a peak the prompt states ("peaking at 2,000 per second"), which the server also reads. Unset and unstated, the design assumes
|
|
1306
|
+
'Wins over a peak the prompt states ("peaking at 2,000 per second"), which the server also reads. Unset and unstated, the design assumes a typical peak for its scale.'
|
|
1253
1307
|
),
|
|
1254
1308
|
data_retention: z4.enum(["days", "months", "years", "indefinite"]).optional(),
|
|
1255
1309
|
consistency: z4.enum(["strict", "eventual", "mixed"]).optional(),
|
|
1256
|
-
data_residency: z4.enum(["any", "us", "eu"]).optional()
|
|
1310
|
+
data_residency: z4.enum(["any", "us", "eu"]).optional(),
|
|
1311
|
+
transfer_band: z4.enum(["light", "moderate", "heavy", "very_heavy", "massive"]).optional().describe(
|
|
1312
|
+
"How much data the system sends out to its users a month: light ~100 GB, moderate ~1 TB, heavy ~10 TB, very_heavy ~100 TB, massive ~1 PB. Priced at the cloud's published per-GB rates. Unset, data transfer is left out of the price and the estimate says so. Set this or monthly_transfer_gb, not both."
|
|
1313
|
+
),
|
|
1314
|
+
monthly_transfer_gb: z4.number().min(0).max(1e8).optional().describe(
|
|
1315
|
+
"The same thing as a figure: GB a month sent out to users. Set this or transfer_band, not both."
|
|
1316
|
+
)
|
|
1257
1317
|
});
|
|
1258
1318
|
var generateInput = z4.object({
|
|
1259
1319
|
prompt: z4.string().min(1).max(2e4).describe("What to design. Describe the system, its scale and its constraints in prose."),
|
|
@@ -1262,7 +1322,7 @@ var generateInput = z4.object({
|
|
|
1262
1322
|
"Default true: run inline and return the finished design (this takes several minutes). False queues it as a background job and returns a job_id to poll \u2014 not available on every deployment."
|
|
1263
1323
|
),
|
|
1264
1324
|
clarification_answers: z4.record(z4.string(), z4.string()).optional().describe(
|
|
1265
|
-
'Answers to the questions a previous call returned with status "clarification_required", keyed by their ids. Treated as authoritative requirements, so send what the user told you \u2014 ask them rather than guessing.'
|
|
1325
|
+
'Answers to the questions a previous call returned with status "clarification_required", keyed by their ids. Treated as authoritative requirements, so send what the user told you \u2014 ask them rather than guessing. "Not sure yet" is a real answer to the load question and to the data-sent-out question when they do not know the figure.'
|
|
1266
1326
|
),
|
|
1267
1327
|
constraints: constraintsInput.optional().describe(
|
|
1268
1328
|
"Facts about the design that the prose does not have to carry. Anything omitted is assumed by SkeletIQ, and the design says which values were assumed."
|
|
@@ -1278,6 +1338,12 @@ var generateOutput = z4.object({
|
|
|
1278
1338
|
component_count: z4.number().nullable(),
|
|
1279
1339
|
assistant_message: z4.string().nullable(),
|
|
1280
1340
|
degradations: z4.array(z4.string()),
|
|
1341
|
+
/**
|
|
1342
|
+
* The server's sentence when part of what the model was given had to be cut to fit its input
|
|
1343
|
+
* window — a source left out, or the current design summarised — and null otherwise. The design
|
|
1344
|
+
* is still complete; this says what it was written without.
|
|
1345
|
+
*/
|
|
1346
|
+
prompt_trim_note: z4.string().nullable(),
|
|
1281
1347
|
/**
|
|
1282
1348
|
* Present, and non-empty, only on `status: "clarification_required"`. A refusal is a
|
|
1283
1349
|
* *result* here rather than an error: an error result may carry no `structuredContent` at
|
|
@@ -1325,7 +1391,7 @@ function registerGenerate(server, client, resolver) {
|
|
|
1325
1391
|
"generate_architecture",
|
|
1326
1392
|
{
|
|
1327
1393
|
title: "Generate a SkeletIQ architecture",
|
|
1328
|
-
description:
|
|
1394
|
+
description: `Design a system architecture from a prompt. This spends the account holder's credits and takes several minutes, so do not call it speculatively \u2014 if a design already exists, read it with get_design instead. Which model runs it is the account holder's stored setting; you do not choose it. Describe the system, what it must do, the load it carries and the constraints it runs under. A prompt that names no system to design comes back as clarification_required, and so does one that states no load: a request rate ("about 300 requests a second"), a fleet (how many devices, and how often each one reports) or a volume ("2 million orders a day"). A headcount alone is not a load. Answer with clarification_answers, keyed by the question ids. Ask your human for the load first. If they do not know it, answer the load question "Not sure yet": the design is then sized on a typical load for its kind of system and marked assumed. Never guess a load or pick a band on their behalf \u2014 a design sized on an invented load is priced and judged against a number nobody chose.`,
|
|
1329
1395
|
inputSchema: generateInput,
|
|
1330
1396
|
outputSchema: generateOutput,
|
|
1331
1397
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
@@ -1369,7 +1435,7 @@ function registerGenerate(server, client, resolver) {
|
|
|
1369
1435
|
error: job.error ?? null,
|
|
1370
1436
|
queue_state: job.queue_state ?? null,
|
|
1371
1437
|
refusal_code: refusal?.code ?? null,
|
|
1372
|
-
clarifying_questions: questions
|
|
1438
|
+
clarifying_questions: questions.map(structuredQuestion)
|
|
1373
1439
|
};
|
|
1374
1440
|
const lines = refusal ? refusalLines(job.job_id, refusal, questions) : [`Generation ${job.job_id} is ${job.status}.`];
|
|
1375
1441
|
if (job.status === "completed" && job.project_id) {
|
|
@@ -1387,14 +1453,39 @@ function decisionRefusal(progress) {
|
|
|
1387
1453
|
return refusal.success ? refusal.data : void 0;
|
|
1388
1454
|
}
|
|
1389
1455
|
function refusalQuestions(refusal) {
|
|
1390
|
-
return (refusal?.clarifying_questions ?? []).map(
|
|
1456
|
+
return (refusal?.clarifying_questions ?? []).map(offeredQuestion);
|
|
1457
|
+
}
|
|
1458
|
+
function offeredQuestion(question) {
|
|
1459
|
+
const options = answerOptions(question);
|
|
1460
|
+
const details = new Map((question.option_details ?? []).map((detail) => [detail.value, detail]));
|
|
1461
|
+
return {
|
|
1391
1462
|
id: question.id,
|
|
1392
1463
|
question: question.question,
|
|
1393
1464
|
why: question.why ?? "",
|
|
1394
|
-
options
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1465
|
+
options,
|
|
1466
|
+
explained: options.flatMap((option) => {
|
|
1467
|
+
const detail = details.get(option);
|
|
1468
|
+
if (!detail) return [];
|
|
1469
|
+
const said = detail.label && detail.label !== option ? `${option} (${detail.label})` : option;
|
|
1470
|
+
return [detail.detail ? `${said}: ${detail.detail}` : said];
|
|
1471
|
+
}),
|
|
1472
|
+
skipEffect: question.skip_effect ?? ""
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function structuredQuestion(question) {
|
|
1476
|
+
return { id: question.id, question: question.question, why: question.why, options: question.options };
|
|
1477
|
+
}
|
|
1478
|
+
function questionLines(questions) {
|
|
1479
|
+
return questions.flatMap((question) => [
|
|
1480
|
+
`${question.id}: ${question.question}`,
|
|
1481
|
+
...question.why ? [` Why it matters: ${question.why}`] : [],
|
|
1482
|
+
...question.explained.length > 0 ? [" Answers:", ...question.explained.map((line) => ` - ${line}`)] : question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : [],
|
|
1483
|
+
...question.skipEffect ? [` If it is left unanswered: ${question.skipEffect}`] : []
|
|
1484
|
+
]);
|
|
1485
|
+
}
|
|
1486
|
+
function answerOptions(question) {
|
|
1487
|
+
const excluded = /* @__PURE__ */ new Set([...question.decline_options ?? [], ...question.dismiss_options ?? []]);
|
|
1488
|
+
return (question.options ?? []).filter((option) => !excluded.has(option));
|
|
1398
1489
|
}
|
|
1399
1490
|
function refusalLines(jobId, refusal, questions) {
|
|
1400
1491
|
const preamble = `Generation ${jobId} was declined before it ran. Nothing was generated and nothing was charged.`;
|
|
@@ -1403,11 +1494,7 @@ function refusalLines(jobId, refusal, questions) {
|
|
|
1403
1494
|
return [
|
|
1404
1495
|
preamble,
|
|
1405
1496
|
"",
|
|
1406
|
-
...questions
|
|
1407
|
-
`${question.id}: ${question.question}`,
|
|
1408
|
-
...question.why ? [` Why it matters: ${question.why}`] : [],
|
|
1409
|
-
...question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : []
|
|
1410
|
-
]),
|
|
1497
|
+
...questionLines(questions),
|
|
1411
1498
|
"",
|
|
1412
1499
|
"Call generate_architecture again with the same prompt and clarification_answers keyed by those ids. Take the answers from the user rather than inventing them."
|
|
1413
1500
|
];
|
|
@@ -1432,22 +1519,14 @@ function clarifyingQuestions(error) {
|
|
|
1432
1519
|
return questions && questions.length > 0 ? questions : void 0;
|
|
1433
1520
|
}
|
|
1434
1521
|
function clarificationResult(questions) {
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
question: question.question,
|
|
1438
|
-
why: question.why ?? "",
|
|
1439
|
-
options: (question.options ?? []).filter((option) => !(question.decline_options ?? []).includes(option))
|
|
1440
|
-
}));
|
|
1522
|
+
const offered = questions.map(offeredQuestion);
|
|
1523
|
+
const structured = offered.map(structuredQuestion);
|
|
1441
1524
|
const lines = [
|
|
1442
|
-
"SkeletIQ will not design from this prompt yet \u2014 it is missing something the design would otherwise have to invent, such as the system itself or the
|
|
1525
|
+
"SkeletIQ will not design from this prompt yet \u2014 it is missing something the design would otherwise have to invent, such as the system itself or the load it carries. Nothing was generated and nothing was charged.",
|
|
1443
1526
|
"",
|
|
1444
|
-
...
|
|
1445
|
-
`${question.id}: ${question.question}`,
|
|
1446
|
-
...question.why ? [` Why it matters: ${question.why}`] : [],
|
|
1447
|
-
...question.options.length > 0 ? [` For example: ${question.options.join(" \xB7 ")}`] : []
|
|
1448
|
-
]),
|
|
1527
|
+
...questionLines(offered),
|
|
1449
1528
|
"",
|
|
1450
|
-
"Call generate_architecture again with the same prompt and clarification_answers keyed by those ids. The answers are used as authoritative requirements, so take them from the user rather than inventing them \u2014 and put anything you already know into constraints instead, where it is used as a fact rather than as prose to interpret."
|
|
1529
|
+
"Call generate_architecture again with the same prompt and clarification_answers keyed by those ids. The answers are used as authoritative requirements, so take them from the user rather than inventing them \u2014 and put anything you already know into constraints instead, where it is used as a fact rather than as prose to interpret." + (offered.some((question) => question.options.includes(LOAD_NOT_SURE)) ? ` If they do not know a figure asked for, answer "${LOAD_NOT_SURE}" rather than picking one.` : "")
|
|
1451
1530
|
];
|
|
1452
1531
|
return ok(
|
|
1453
1532
|
{
|
|
@@ -1460,6 +1539,7 @@ function clarificationResult(questions) {
|
|
|
1460
1539
|
component_count: null,
|
|
1461
1540
|
assistant_message: null,
|
|
1462
1541
|
degradations: [],
|
|
1542
|
+
prompt_trim_note: null,
|
|
1463
1543
|
clarifying_questions: structured
|
|
1464
1544
|
},
|
|
1465
1545
|
lines.join("\n")
|
|
@@ -1480,6 +1560,7 @@ async function queueGeneration(client, body) {
|
|
|
1480
1560
|
component_count: null,
|
|
1481
1561
|
assistant_message: null,
|
|
1482
1562
|
degradations: [],
|
|
1563
|
+
prompt_trim_note: null,
|
|
1483
1564
|
clarifying_questions: []
|
|
1484
1565
|
},
|
|
1485
1566
|
`Queued as job ${queued.job_id}. Poll it with get_generation_status.`
|
|
@@ -1528,6 +1609,7 @@ async function streamGeneration(client, body, ctx, resolver) {
|
|
|
1528
1609
|
const architectureId = typeof finished.architecture_id === "string" ? finished.architecture_id : null;
|
|
1529
1610
|
const version = typeof finished.architecture_version === "number" ? finished.architecture_version : null;
|
|
1530
1611
|
const degradations = Array.isArray(finished.degradations) ? finished.degradations.filter((d) => typeof d === "string") : [];
|
|
1612
|
+
const trimNote = asText(asRecord2(finished.run_summary)?.prompt_trim_note) ?? null;
|
|
1531
1613
|
const created = architectureId ? await describeCreated(resolver, architectureId) : void 0;
|
|
1532
1614
|
return ok(
|
|
1533
1615
|
{
|
|
@@ -1540,6 +1622,7 @@ async function streamGeneration(client, body, ctx, resolver) {
|
|
|
1540
1622
|
component_count: created?.componentCount ?? null,
|
|
1541
1623
|
assistant_message: null,
|
|
1542
1624
|
degradations,
|
|
1625
|
+
prompt_trim_note: trimNote,
|
|
1543
1626
|
clarifying_questions: []
|
|
1544
1627
|
},
|
|
1545
1628
|
[
|
|
@@ -1549,7 +1632,8 @@ async function streamGeneration(client, body, ctx, resolver) {
|
|
|
1549
1632
|
'Check get_design(mode: "readiness") before building \u2014 a fresh design is a draft, and',
|
|
1550
1633
|
"its open questions are the ones worth asking now."
|
|
1551
1634
|
] : ["SkeletIQ answered in prose rather than producing a design."],
|
|
1552
|
-
...degradations.map((d) => `Degraded: ${d}`)
|
|
1635
|
+
...degradations.map((d) => `Degraded: ${d}`),
|
|
1636
|
+
...trimNote ? [`Input trimmed: ${trimNote}`] : []
|
|
1553
1637
|
].join("\n")
|
|
1554
1638
|
);
|
|
1555
1639
|
}
|
|
@@ -1603,10 +1687,30 @@ function isCitedRequirement(row) {
|
|
|
1603
1687
|
return (row.cited_by_components?.length ?? 0) > 0 || (row.cited_by_decisions?.length ?? 0) > 0;
|
|
1604
1688
|
}
|
|
1605
1689
|
var CITATION_ONLY_METHOD = "structure";
|
|
1690
|
+
var MEASURED_METHODS = ["simulation", "cost", "rules"];
|
|
1691
|
+
var UNCONFIRMED_WORDS = {
|
|
1692
|
+
model_check: "unconfirmed (model check)",
|
|
1693
|
+
unstated_input: "unconfirmed (unstated split)",
|
|
1694
|
+
advisory_finding: "unconfirmed (review advice)"
|
|
1695
|
+
};
|
|
1696
|
+
var ASSUMED_LOAD_INPUT = "the load, which was assumed";
|
|
1697
|
+
var ASSUMED_LOAD_WORD = "unconfirmed (assumed load)";
|
|
1698
|
+
var UNCONFIRMED_ADVICE = "verify before building for it";
|
|
1699
|
+
function requirementBlocksHandoff(row) {
|
|
1700
|
+
if (typeof row.blocks_handoff === "boolean") return row.blocks_handoff;
|
|
1701
|
+
return (row.verdict ?? "").trim() === "fails" && MEASURED_METHODS.includes((row.check_method ?? "").trim());
|
|
1702
|
+
}
|
|
1606
1703
|
function requirementVerdict(row) {
|
|
1607
1704
|
const verdict = (row.verdict ?? row.status ?? "").trim();
|
|
1608
|
-
if (verdict
|
|
1609
|
-
|
|
1705
|
+
if (verdict !== "fails" || requirementBlocksHandoff(row)) return verdict;
|
|
1706
|
+
const method = (row.check_method ?? "").trim();
|
|
1707
|
+
if (method === CITATION_ONLY_METHOD) return "";
|
|
1708
|
+
const reason = row.unconfirmed_reason ?? "model_check";
|
|
1709
|
+
if (reason === "unstated_input" && row.unstated_input?.trim() === ASSUMED_LOAD_INPUT) return ASSUMED_LOAD_WORD;
|
|
1710
|
+
return UNCONFIRMED_WORDS[reason] ?? "unconfirmed";
|
|
1711
|
+
}
|
|
1712
|
+
function isUnconfirmedRequirement(row) {
|
|
1713
|
+
return requirementVerdict(row).startsWith("unconfirmed");
|
|
1610
1714
|
}
|
|
1611
1715
|
function orderRequirementRows(rows) {
|
|
1612
1716
|
return [...rows].sort((a, b) => verdictRank(a) - verdictRank(b));
|
|
@@ -1614,9 +1718,10 @@ function orderRequirementRows(rows) {
|
|
|
1614
1718
|
function verdictRank(row) {
|
|
1615
1719
|
const verdict = requirementVerdict(row);
|
|
1616
1720
|
if (verdict === "fails" || verdict === "unmet") return 0;
|
|
1617
|
-
if (
|
|
1721
|
+
if (isUnconfirmedRequirement(row)) return 1;
|
|
1722
|
+
if (!isCitedRequirement(row)) return 2;
|
|
1618
1723
|
const index = VERDICT_ORDER.indexOf(verdict);
|
|
1619
|
-
return index === -1 ? VERDICT_ORDER.length +
|
|
1724
|
+
return index === -1 ? VERDICT_ORDER.length + 2 : index + 3;
|
|
1620
1725
|
}
|
|
1621
1726
|
function decisionTexts(design) {
|
|
1622
1727
|
return decisionEntries(design).map((entry) => entry.text);
|
|
@@ -1874,6 +1979,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1874
1979
|
const gates = readiness.rows.filter((row) => row.category === "gate");
|
|
1875
1980
|
const open = gates.filter((row) => row.gating);
|
|
1876
1981
|
const unanswered = gates.filter((row) => row.state === "unknown");
|
|
1982
|
+
const unconfirmedGates = gates.filter((row) => row.state === "unconfirmed");
|
|
1877
1983
|
const accepts = readiness.rows.filter((row) => row.category === "accept");
|
|
1878
1984
|
const advisories = readiness.rows.filter(
|
|
1879
1985
|
(row) => row.category !== "gate" && row.category !== "accept"
|
|
@@ -1885,6 +1991,11 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1885
1991
|
return ok({ ...base, ...facts, data: readiness }, [
|
|
1886
1992
|
headline,
|
|
1887
1993
|
...listed.map((row) => `- ${row.label}: ${describeRow(row)}`),
|
|
1994
|
+
...unconfirmedGates.length ? [
|
|
1995
|
+
"",
|
|
1996
|
+
"Unconfirmed (the check ran, but its answer turns on an input nobody stated; never blocks):",
|
|
1997
|
+
...unconfirmedGates.map((row) => `- ${row.label}: ${describeRow(row)}`)
|
|
1998
|
+
] : [],
|
|
1888
1999
|
...accepts.length ? [
|
|
1889
2000
|
"",
|
|
1890
2001
|
"To accept (the person agrees to these rather than fixing them; accepting is free and never blocks):",
|
|
@@ -1945,11 +2056,11 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1945
2056
|
const orphaned = gaps.orphaned_answers ?? [];
|
|
1946
2057
|
return ok({ ...base, ...facts, data: gaps }, [
|
|
1947
2058
|
unresolved.length === 0 ? "Every open question and assumption in this design has been dealt with." : `${unresolved.length} unresolved:`,
|
|
1948
|
-
...unresolved.map((gap) => `- [${gap.kind}] ${gap.text}`),
|
|
2059
|
+
...unresolved.map((gap) => `- [${gap.kind}] ${gap.text}${sizingNote(gap)}`),
|
|
1949
2060
|
...settled.length > 0 ? [
|
|
1950
2061
|
"",
|
|
1951
2062
|
`${settled.length} settled during review \u2014 treat these as decided:`,
|
|
1952
|
-
...settled.map((gap) => `- [${gap.kind}] ${gap.text} \u2192 ${settledAnswer(gap)}`)
|
|
2063
|
+
...settled.map((gap) => `- [${gap.kind}] ${gap.text} \u2192 ${settledAnswer(gap)}${sizingNote(gap)}`)
|
|
1953
2064
|
] : [],
|
|
1954
2065
|
// A gap is identified by a hash of its own text, so a regeneration that
|
|
1955
2066
|
// re-words a question strands the answer that settled it. The server
|
|
@@ -1975,8 +2086,10 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1975
2086
|
function requirementBlock(ledger) {
|
|
1976
2087
|
const rows = orderRequirementRows(ledger?.rows ?? []);
|
|
1977
2088
|
if (!ledger || rows.length === 0) return [];
|
|
2089
|
+
const unconfirmed = rows.filter(isUnconfirmedRequirement).length;
|
|
1978
2090
|
const attention = [
|
|
1979
2091
|
[rows.filter((row) => requirementVerdict(row) === "fails").length, "failing"],
|
|
2092
|
+
[unconfirmed, "unconfirmed"],
|
|
1980
2093
|
[rows.filter((row) => !isCitedRequirement(row)).length, "cited by nothing"],
|
|
1981
2094
|
[rows.filter((row) => requirementVerdict(row) === "not_checked" || requirementVerdict(row) === "not_checkable").length, "unchecked"]
|
|
1982
2095
|
].filter(([count]) => count > 0).map(([count, word]) => `${count} ${word}`);
|
|
@@ -1989,6 +2102,7 @@ function requirementBlock(ledger) {
|
|
|
1989
2102
|
"contradicted it, which is not the same as proven; `not_checked` / `not_checkable` \u2014 nobody",
|
|
1990
2103
|
"established either way, and neither is fine. `cited by nothing` means no component and no",
|
|
1991
2104
|
"decision in the design claims to address it.",
|
|
2105
|
+
...unconfirmedLegend(rows),
|
|
1992
2106
|
// Two facts about how far the statuses can be trusted, said only when the server states
|
|
1993
2107
|
// them. An absent `verification_state` is an older server, not a verifier that did not run,
|
|
1994
2108
|
// and claiming the second would be this package inventing a fact about someone's design.
|
|
@@ -2006,6 +2120,34 @@ function requirementBlock(ledger) {
|
|
|
2006
2120
|
...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
|
|
2007
2121
|
];
|
|
2008
2122
|
}
|
|
2123
|
+
function unconfirmedLegend(rows) {
|
|
2124
|
+
const words = new Set(rows.filter(isUnconfirmedRequirement).map(requirementVerdict));
|
|
2125
|
+
const reasons = new Set(
|
|
2126
|
+
rows.filter((row) => isUnconfirmedRequirement(row) && requirementVerdict(row) !== "unconfirmed (assumed load)").map((row) => row.unconfirmed_reason ?? "model_check")
|
|
2127
|
+
);
|
|
2128
|
+
return [
|
|
2129
|
+
...reasons.has("model_check") ? [
|
|
2130
|
+
"`unconfirmed (model check)` \u2014 only a model check says the design may not meet it. Nothing",
|
|
2131
|
+
`measured it, so it does not block handoff: ${UNCONFIRMED_ADVICE} rather than adding a part.`
|
|
2132
|
+
] : [],
|
|
2133
|
+
...reasons.has("unstated_input") ? [
|
|
2134
|
+
"`unconfirmed (unstated split)` \u2014 capacity falls short only under an assumption nobody stated",
|
|
2135
|
+
"(the rate split evenly between entrances, or traffic weights that do not add up). Its reason",
|
|
2136
|
+
"names the split the design would need \u2014 judge whether that is realistic. It does not block",
|
|
2137
|
+
"handoff: ask how the traffic divides."
|
|
2138
|
+
] : [],
|
|
2139
|
+
...reasons.has("advisory_finding") ? [
|
|
2140
|
+
"`unconfirmed (review advice)` \u2014 only advice from the design review fails it: the findings it",
|
|
2141
|
+
"rests on are reported, not scored, so it does not block handoff. Read the advice and decide",
|
|
2142
|
+
"whether the design should take it."
|
|
2143
|
+
] : [],
|
|
2144
|
+
...words.has("unconfirmed (assumed load)") ? [
|
|
2145
|
+
"`unconfirmed (assumed load)` \u2014 the verdict rests on the load, which was assumed: nobody said how",
|
|
2146
|
+
"much traffic to expect, so the design was sized and priced on the typical load for this kind of",
|
|
2147
|
+
"system. It does not block handoff: ask your human for their load rather than guessing one."
|
|
2148
|
+
] : []
|
|
2149
|
+
];
|
|
2150
|
+
}
|
|
2009
2151
|
function requirementsServed(rows) {
|
|
2010
2152
|
if (!rows?.length) return [];
|
|
2011
2153
|
return [
|
|
@@ -2013,7 +2155,8 @@ function requirementsServed(rows) {
|
|
|
2013
2155
|
"Requirements the design cites this component for:",
|
|
2014
2156
|
...rows.map(requirementLine),
|
|
2015
2157
|
"`fails` \u2014 a named check measured the design as not meeting it; `not_checked` \u2014 nobody",
|
|
2016
|
-
"established either way; `no_gap_found` \u2014 nothing contradicted it, which is not the same as proven."
|
|
2158
|
+
"established either way; `no_gap_found` \u2014 nothing contradicted it, which is not the same as proven.",
|
|
2159
|
+
...unconfirmedLegend(rows)
|
|
2017
2160
|
];
|
|
2018
2161
|
}
|
|
2019
2162
|
function requirementsWithdrawn(rows) {
|
|
@@ -2028,7 +2171,8 @@ function requirementsWithdrawn(rows) {
|
|
|
2028
2171
|
}
|
|
2029
2172
|
function requirementLine(row) {
|
|
2030
2173
|
const verdict = requirementVerdict(row);
|
|
2031
|
-
const
|
|
2174
|
+
const shown = isUnconfirmedRequirement(row) ? `${verdict} \u2014 ${UNCONFIRMED_ADVICE}` : verdict;
|
|
2175
|
+
const marks = [shown, ...isCitedRequirement(row) ? [] : ["cited by nothing"]].filter(Boolean);
|
|
2032
2176
|
const label = marks.length > 0 ? ` [${marks.join(" \xB7 ")}]` : "";
|
|
2033
2177
|
const text = (row.text ?? "").replace(/\s+/g, " ").trim();
|
|
2034
2178
|
const reason = (row.verdict_reason ?? "").replace(/\s+/g, " ").trim();
|
|
@@ -2046,6 +2190,10 @@ function settledAnswer(gap) {
|
|
|
2046
2190
|
if (gap.action === "confirmed") return "confirmed as it stands";
|
|
2047
2191
|
return "answered, but no wording was recorded \u2014 ask before relying on it";
|
|
2048
2192
|
}
|
|
2193
|
+
function sizingNote(gap) {
|
|
2194
|
+
const conflict = gap.sizing_conflict;
|
|
2195
|
+
return conflict ? ` (this design is sized for ${conflict.sized}, not ${conflict.assumed})` : "";
|
|
2196
|
+
}
|
|
2049
2197
|
function todoGroupBlock(group) {
|
|
2050
2198
|
if (group.items.length === 0) return [];
|
|
2051
2199
|
return ["", `${group.label} (${group.items.length}):`, ...group.items.map(todoLine)];
|
|
@@ -2059,7 +2207,12 @@ function todoLine(item) {
|
|
|
2059
2207
|
return `- ${severity}${item.title.replace(/\s+/g, " ").trim()}${components}${raised}${carried}`;
|
|
2060
2208
|
}
|
|
2061
2209
|
function describeRow(row) {
|
|
2062
|
-
if (row.state === "unknown")
|
|
2210
|
+
if (row.state === "unknown") {
|
|
2211
|
+
const clears = row.clears ? ` ${row.clears}` : "";
|
|
2212
|
+
const detail = row.detail ? ` \u2014 ${row.detail}${clears && !/[.!?]$/.test(row.detail) ? "." : ""}` : clears ? " \u2014" : "";
|
|
2213
|
+
return `not checked${detail}${clears}`;
|
|
2214
|
+
}
|
|
2215
|
+
if (row.state === "unconfirmed") return `unconfirmed${row.detail ? ` \u2014 ${row.detail}` : ""}`;
|
|
2063
2216
|
return `${row.count}${row.detail ? ` \u2014 ${row.detail}` : ""}`;
|
|
2064
2217
|
}
|
|
2065
2218
|
|
package/package.json
CHANGED