@skeletiq/mcp 0.1.2 → 0.3.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 +473 -69
- 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`. 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
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.3.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,88 @@ 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
|
+
/** Which check answered this line: `simulation` | `cost` | `rules` | `structure` | `verifier`.
|
|
335
|
+
* Four are deterministic; `verifier` is the model check, and it only owns a row when it gave a
|
|
336
|
+
* reason. A string rather than a union, for the reason `status` is one. Absent from older APIs. */
|
|
337
|
+
check_method: z.string().nullable().optional(),
|
|
338
|
+
/** What that check said: `fails` | `passes` | `no_gap_found` | `not_checkable` | `not_checked`.
|
|
339
|
+
* `passes` is only ever a measurement; `no_gap_found` is the weaker "nothing contradicted it".
|
|
340
|
+
* Reported beside `status`, which stays populated. Absent from older APIs. */
|
|
341
|
+
verdict: z.string().nullable().optional(),
|
|
342
|
+
/** Why, in one sentence, with the figure or the finding behind it. */
|
|
343
|
+
verdict_reason: z.string().nullable().optional(),
|
|
344
|
+
/** The model behind a `verifier` verdict. Null on every deterministic row — naming a model
|
|
345
|
+
* beside a number the server computed would be a false attribution. */
|
|
346
|
+
checked_by: z.string().nullable().optional(),
|
|
347
|
+
cited_by_components: z.array(z.string()).nullable().optional(),
|
|
348
|
+
/** Indices into `architecture_json.grounded_decisions`, the raw array — blanks are not compacted. */
|
|
349
|
+
cited_by_decisions: z.array(z.number()).nullable().optional(),
|
|
350
|
+
added_in_version: z.number().nullable().optional(),
|
|
351
|
+
/** Set only on a row under `RequirementLedger.superseded`: an instruction an earlier analysis
|
|
352
|
+
* recorded as a requirement, now the change request it names (`CR5`). Absent from older APIs. */
|
|
353
|
+
superseded_by_change_request: z.string().nullable().optional(),
|
|
354
|
+
superseded_in_version: z.number().nullable().optional(),
|
|
355
|
+
/** What a person said about this line: `confirmed` ("yes, that one is real") or `removed`
|
|
356
|
+
* ("that is not a requirement"). Null or absent means nobody has said, which is neither. A
|
|
357
|
+
* `removed` row is listed under `RequirementLedger.removed` and never in `rows`, so a design
|
|
358
|
+
* is not held to it; it keeps its id so a citation naming it still resolves. `confirmed`
|
|
359
|
+
* changes no count and no verdict. Absent from older APIs. */
|
|
360
|
+
decision: z.string().nullable().optional(),
|
|
361
|
+
/** The version that was on screen when that decision was recorded. */
|
|
362
|
+
decided_in_version: z.number().nullable().optional()
|
|
363
|
+
});
|
|
364
|
+
var RequirementLedgerCountsSchema = z.looseObject({
|
|
365
|
+
total: z.number().optional(),
|
|
366
|
+
user_supplied: z.number().optional(),
|
|
367
|
+
llm_inferred: z.number().optional(),
|
|
368
|
+
unmet: z.number().optional(),
|
|
369
|
+
uncited: z.number().optional(),
|
|
370
|
+
unchecked: z.number().optional(),
|
|
371
|
+
cited: z.number().optional(),
|
|
372
|
+
/** Rows citing no component and no decision (overlaps `unmet`). Absent from older APIs. */
|
|
373
|
+
untraced: z.number().optional(),
|
|
374
|
+
/** The verdict partition, which sums to `total` exactly as the status partition above does —
|
|
375
|
+
* two readings of one set of rows, not two sets. Absent from older APIs. */
|
|
376
|
+
fails: z.number().optional(),
|
|
377
|
+
passes: z.number().optional(),
|
|
378
|
+
no_gap_found: z.number().optional(),
|
|
379
|
+
not_checkable: z.number().optional(),
|
|
380
|
+
not_checked: z.number().optional(),
|
|
381
|
+
/** Rows a person has confirmed are real requirements. In no partition — it overlaps all of
|
|
382
|
+
* them, because confirming a line says nothing about whether the design meets it. */
|
|
383
|
+
confirmed: z.number().optional(),
|
|
384
|
+
/** Ids a person said are **not** requirements. Not part of `total`: those rows are not in
|
|
385
|
+
* `rows` at all. Absent from older APIs. */
|
|
386
|
+
removed: z.number().optional()
|
|
387
|
+
});
|
|
388
|
+
var RequirementLedgerSchema = z.looseObject({
|
|
389
|
+
rows: z.array(RequirementLedgerRowSchema).nullable().optional(),
|
|
390
|
+
/** Ids that were instructions, not requirements. Counted nowhere; listed so an id a component
|
|
391
|
+
* or decision still cites resolves to what became of it. Absent from older APIs. */
|
|
392
|
+
superseded: z.array(RequirementLedgerRowSchema).nullable().optional(),
|
|
393
|
+
/** Ids a person said are not requirements, so the design is not held to them. Kept apart from
|
|
394
|
+
* `superseded` because it is a different statement by a different author. Counted nowhere;
|
|
395
|
+
* listed for the same reason, so a citation naming one still resolves. Absent from older
|
|
396
|
+
* APIs. */
|
|
397
|
+
removed: z.array(RequirementLedgerRowSchema).nullable().optional(),
|
|
398
|
+
analysis_version: z.number().nullable().optional(),
|
|
399
|
+
analysis_degraded: z.boolean().optional(),
|
|
400
|
+
/** `grounded` | `not_grounded` | `diverged`. */
|
|
401
|
+
grounding_state: z.string().optional(),
|
|
402
|
+
/** `ran` | `partial` | `not_run`. */
|
|
403
|
+
verification_state: z.string().optional(),
|
|
404
|
+
verified_version: z.number().nullable().optional(),
|
|
405
|
+
counts: RequirementLedgerCountsSchema.nullable().optional()
|
|
406
|
+
});
|
|
325
407
|
var ArchitectureSchema = z.looseObject({
|
|
326
408
|
id: z.string(),
|
|
327
409
|
project_id: z.string(),
|
|
@@ -332,7 +414,8 @@ var ArchitectureSchema = z.looseObject({
|
|
|
332
414
|
created_at: z.string().optional(),
|
|
333
415
|
updated_at: z.string().optional(),
|
|
334
416
|
is_released: z.boolean().optional(),
|
|
335
|
-
released_at: z.string().nullable().optional()
|
|
417
|
+
released_at: z.string().nullable().optional(),
|
|
418
|
+
requirement_ledger: RequirementLedgerSchema.nullable().optional()
|
|
336
419
|
});
|
|
337
420
|
var ArchitecturePageSchema = z.looseObject({
|
|
338
421
|
items: z.array(ArchitectureSchema),
|
|
@@ -365,6 +448,12 @@ var BuildOrderSchema = z.looseObject({
|
|
|
365
448
|
steps: z.array(BuildStepSchema),
|
|
366
449
|
release: ReleaseFactsSchema
|
|
367
450
|
});
|
|
451
|
+
var UnavailableSourceSchema = z.looseObject({
|
|
452
|
+
source: z.string(),
|
|
453
|
+
reason: z.string(),
|
|
454
|
+
/** `failed` | `missing`. A string, not a union: a value added server-side must still parse. */
|
|
455
|
+
kind: z.string().optional()
|
|
456
|
+
});
|
|
368
457
|
var ReadinessRowSchema = z.looseObject({
|
|
369
458
|
key: z.string(),
|
|
370
459
|
label: z.string(),
|
|
@@ -403,7 +492,16 @@ var ReadinessSchema = z.looseObject({
|
|
|
403
492
|
* Optional here only so a server that predates the field still parses — it is required on the
|
|
404
493
|
* wire, and an empty array is the server saying it looked and found nothing.
|
|
405
494
|
*/
|
|
406
|
-
release_warnings: z.array(z.string()).optional()
|
|
495
|
+
release_warnings: z.array(z.string()).optional(),
|
|
496
|
+
/**
|
|
497
|
+
* Sources this checklist could not speak for, exactly as `/todo` reports them.
|
|
498
|
+
*
|
|
499
|
+
* A failed read reaches each row builder as that source's own empty value — no questions, no
|
|
500
|
+
* decisions, no ledger — and every one of those empties reads as *clear*, so an outage could
|
|
501
|
+
* answer `ready: true` with `release_warnings: []`. Each one also has a row of its own now;
|
|
502
|
+
* this is the same fact in the shape a caller showing both lists can join on.
|
|
503
|
+
*/
|
|
504
|
+
unavailable_sources: z.array(UnavailableSourceSchema).optional()
|
|
407
505
|
});
|
|
408
506
|
var DesignGapSchema = z.looseObject({
|
|
409
507
|
gap_id: z.string(),
|
|
@@ -412,7 +510,13 @@ var DesignGapSchema = z.looseObject({
|
|
|
412
510
|
resolved: z.boolean(),
|
|
413
511
|
action: z.string().nullable().optional(),
|
|
414
512
|
note: z.string().nullable().optional(),
|
|
415
|
-
adr_id: z.string().nullable().optional()
|
|
513
|
+
adr_id: z.string().nullable().optional(),
|
|
514
|
+
/** `gap:{gap_id}` — the identity the review, the advisor and the decision suggestions share,
|
|
515
|
+
* and the key a dismissal is recorded against. Absent from servers before 0.3.0. */
|
|
516
|
+
problem_key: z.string().optional(),
|
|
517
|
+
/** An answer is recorded and this version was not built with it, so iterating the design
|
|
518
|
+
* would change something. False once a generation has been handed the answer. */
|
|
519
|
+
answer_pending: z.boolean().optional()
|
|
416
520
|
});
|
|
417
521
|
var OrphanedAnswerSchema = z.looseObject({
|
|
418
522
|
gap_id: z.string(),
|
|
@@ -430,29 +534,6 @@ var DesignGapListSchema = z.looseObject({
|
|
|
430
534
|
/** Answers whose question is not in this version — recorded, not silently dropped. */
|
|
431
535
|
orphaned_answers: z.array(OrphanedAnswerSchema).optional()
|
|
432
536
|
});
|
|
433
|
-
var ComponentRefSchema = z.looseObject({
|
|
434
|
-
id: z.string(),
|
|
435
|
-
name: z.string(),
|
|
436
|
-
type: z.string()
|
|
437
|
-
});
|
|
438
|
-
var UnknownIdSchema = z.looseObject({
|
|
439
|
-
id: z.string(),
|
|
440
|
-
suggestions: z.array(ComponentRefSchema).optional()
|
|
441
|
-
});
|
|
442
|
-
var DriftSchema = z.looseObject({
|
|
443
|
-
architecture_id: z.string(),
|
|
444
|
-
version: z.number(),
|
|
445
|
-
in_sync: z.boolean(),
|
|
446
|
-
missing: z.array(ComponentRefSchema),
|
|
447
|
-
unreported: z.array(ComponentRefSchema),
|
|
448
|
-
partial: z.array(ComponentRefSchema),
|
|
449
|
-
elsewhere: z.array(ComponentRefSchema),
|
|
450
|
-
unknown_ids: z.array(UnknownIdSchema),
|
|
451
|
-
extra_components: z.array(z.looseObject({ name: z.string() })),
|
|
452
|
-
extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
|
|
453
|
-
counts: z.record(z.string(), z.number()),
|
|
454
|
-
release: ReleaseFactsSchema
|
|
455
|
-
});
|
|
456
537
|
var CritiqueFindingSchema = z.looseObject({
|
|
457
538
|
message: z.string(),
|
|
458
539
|
pattern_id: z.string(),
|
|
@@ -460,7 +541,17 @@ var CritiqueFindingSchema = z.looseObject({
|
|
|
460
541
|
severity: z.string().optional(),
|
|
461
542
|
affected_components: z.array(z.string()).optional(),
|
|
462
543
|
evidence: z.string().optional(),
|
|
463
|
-
remediation: z.string().optional()
|
|
544
|
+
remediation: z.string().optional(),
|
|
545
|
+
/** Reported in full and never charged against the score — advice, not a problem to fix. */
|
|
546
|
+
advisory_only: z.boolean().optional(),
|
|
547
|
+
/**
|
|
548
|
+
* The problem this finding is about. Stable across rewording, and shared with the advisor
|
|
549
|
+
* suggestion and decision record raised by the same problem — the key a dismissal is recorded
|
|
550
|
+
* against.
|
|
551
|
+
*/
|
|
552
|
+
problem_key: z.string().optional(),
|
|
553
|
+
/** The project set this problem aside, with a reason. Still reported; never set on an unsaved payload. */
|
|
554
|
+
dismissed: z.boolean().optional()
|
|
464
555
|
});
|
|
465
556
|
var PayloadCritiqueSchema = z.looseObject({
|
|
466
557
|
architecture_score: z.number(),
|
|
@@ -478,6 +569,96 @@ var PayloadCritiqueSchema = z.looseObject({
|
|
|
478
569
|
compliance_note: z.string().nullable().optional(),
|
|
479
570
|
exposure_assessed: z.string().optional()
|
|
480
571
|
});
|
|
572
|
+
var TodoDecisionContextSchema = z.looseObject({
|
|
573
|
+
context: z.string().nullable().optional(),
|
|
574
|
+
consequences: z.string().nullable().optional(),
|
|
575
|
+
alternatives: z.string().nullable().optional(),
|
|
576
|
+
/** `records` | `open` | `tradeoff`. A string, not a union: a value added server-side must still parse. */
|
|
577
|
+
disposition: z.string(),
|
|
578
|
+
supersedes_adr_id: z.string().nullable().optional(),
|
|
579
|
+
source_version: z.number()
|
|
580
|
+
});
|
|
581
|
+
var TodoSourceSchema = z.looseObject({
|
|
582
|
+
source: z.string(),
|
|
583
|
+
message: z.string(),
|
|
584
|
+
detail: z.string().nullable().optional(),
|
|
585
|
+
/** A handle to act on through that source: a suggestion id, a gap id, an R-id. */
|
|
586
|
+
reference_id: z.string().nullable().optional(),
|
|
587
|
+
/** Present on a `critique` or `compliance` source; null on every other kind. */
|
|
588
|
+
finding: CritiqueFindingSchema.nullable().optional(),
|
|
589
|
+
/** Present on a `decisions` source; null on every other kind. */
|
|
590
|
+
decision: TodoDecisionContextSchema.nullable().optional()
|
|
591
|
+
});
|
|
592
|
+
var TodoActionSchema = z.looseObject({
|
|
593
|
+
action: z.string(),
|
|
594
|
+
available: z.boolean(),
|
|
595
|
+
reason: z.string().nullable().optional(),
|
|
596
|
+
reference_id: z.string().nullable().optional()
|
|
597
|
+
});
|
|
598
|
+
var TodoItemSchema = z.looseObject({
|
|
599
|
+
problem_key: z.string(),
|
|
600
|
+
group: z.string(),
|
|
601
|
+
/** `problem` | `question` | `assumption` | `decision` | `verdict`. A string, not a union: a
|
|
602
|
+
* value added server-side must not stop an older connector parsing the card. */
|
|
603
|
+
kind: z.string(),
|
|
604
|
+
title: z.string(),
|
|
605
|
+
severity: z.string(),
|
|
606
|
+
detail: z.string().nullable().optional(),
|
|
607
|
+
components: z.array(z.string()).optional(),
|
|
608
|
+
sources: z.array(TodoSourceSchema).optional(),
|
|
609
|
+
actions: z.array(TodoActionSchema).optional(),
|
|
610
|
+
carried: z.boolean().optional(),
|
|
611
|
+
/** The oldest earlier version known to have had this problem. Null with `carried: true` means
|
|
612
|
+
* it goes back further than the server looked — "an earlier version", never version zero. */
|
|
613
|
+
carried_from_version: z.number().nullable().optional(),
|
|
614
|
+
dismissed: z.boolean().optional(),
|
|
615
|
+
dismissed_reason: z.string().nullable().optional()
|
|
616
|
+
});
|
|
617
|
+
var TodoGroupSchema = z.looseObject({
|
|
618
|
+
key: z.string(),
|
|
619
|
+
label: z.string(),
|
|
620
|
+
items: z.array(TodoItemSchema),
|
|
621
|
+
bulk_action: TodoActionSchema.nullable().optional()
|
|
622
|
+
});
|
|
623
|
+
var TodoSchema = z.looseObject({
|
|
624
|
+
architecture_id: z.string(),
|
|
625
|
+
version: z.number(),
|
|
626
|
+
groups: z.array(TodoGroupSchema),
|
|
627
|
+
dismissed: z.array(TodoItemSchema).optional(),
|
|
628
|
+
counts: z.record(z.string(), z.number()).optional(),
|
|
629
|
+
/** Rows the sources raised before grouping. The gap between this and `counts.total` is the
|
|
630
|
+
* duplication the endpoint removes, reported so the claim stays checkable. */
|
|
631
|
+
raw_row_count: z.number().optional(),
|
|
632
|
+
unavailable_sources: z.array(UnavailableSourceSchema).optional(),
|
|
633
|
+
generation_running: z.boolean().optional(),
|
|
634
|
+
can_edit: z.boolean().optional(),
|
|
635
|
+
/** The project's newest version. Above `version` when this list is for an older one — a
|
|
636
|
+
* release resolved behind newer drafts, or a version asked for by number. Null when unread. */
|
|
637
|
+
latest_version: z.number().nullable().optional()
|
|
638
|
+
});
|
|
639
|
+
var ComponentRefSchema = z.looseObject({
|
|
640
|
+
id: z.string(),
|
|
641
|
+
name: z.string(),
|
|
642
|
+
type: z.string()
|
|
643
|
+
});
|
|
644
|
+
var UnknownIdSchema = z.looseObject({
|
|
645
|
+
id: z.string(),
|
|
646
|
+
suggestions: z.array(ComponentRefSchema).optional()
|
|
647
|
+
});
|
|
648
|
+
var DriftSchema = z.looseObject({
|
|
649
|
+
architecture_id: z.string(),
|
|
650
|
+
version: z.number(),
|
|
651
|
+
in_sync: z.boolean(),
|
|
652
|
+
missing: z.array(ComponentRefSchema),
|
|
653
|
+
unreported: z.array(ComponentRefSchema),
|
|
654
|
+
partial: z.array(ComponentRefSchema),
|
|
655
|
+
elsewhere: z.array(ComponentRefSchema),
|
|
656
|
+
unknown_ids: z.array(UnknownIdSchema),
|
|
657
|
+
extra_components: z.array(z.looseObject({ name: z.string() })),
|
|
658
|
+
extra_connections: z.array(z.looseObject({ source: z.string(), target: z.string() })),
|
|
659
|
+
counts: z.record(z.string(), z.number()),
|
|
660
|
+
release: ReleaseFactsSchema
|
|
661
|
+
});
|
|
481
662
|
var ClarifyingQuestionSchema = z.looseObject({
|
|
482
663
|
id: z.string(),
|
|
483
664
|
question: z.string(),
|
|
@@ -546,7 +727,9 @@ var PROJECT_PAGE_SIZE = 50;
|
|
|
546
727
|
var ARCHITECTURE_PAGE_SIZE = 100;
|
|
547
728
|
var DesignResolver = class {
|
|
548
729
|
client;
|
|
730
|
+
/** Version summaries per project — enough to choose a version, never to describe one. */
|
|
549
731
|
versions = /* @__PURE__ */ new Map();
|
|
732
|
+
/** Full records from the single-version read, the requirements ledger included. */
|
|
550
733
|
designs = /* @__PURE__ */ new Map();
|
|
551
734
|
constructor(client) {
|
|
552
735
|
this.client = client;
|
|
@@ -575,7 +758,11 @@ var DesignResolver = class {
|
|
|
575
758
|
if (exact.length === 1) return exact[0];
|
|
576
759
|
throw new AmbiguousProjectError(idOrQuery, candidates);
|
|
577
760
|
}
|
|
578
|
-
/**
|
|
761
|
+
/**
|
|
762
|
+
* Every version of a project, newest first. Cached: one call serves a whole session.
|
|
763
|
+
*
|
|
764
|
+
* These are summaries. They seed nothing in `designs` — see the file header for why.
|
|
765
|
+
*/
|
|
579
766
|
async versionsFor(projectId) {
|
|
580
767
|
const cached = this.versions.get(projectId);
|
|
581
768
|
if (cached) return cached;
|
|
@@ -584,7 +771,6 @@ var DesignResolver = class {
|
|
|
584
771
|
});
|
|
585
772
|
const items = [...ArchitecturePageSchema.parse(body).items].sort((a, b) => b.version - a.version);
|
|
586
773
|
this.versions.set(projectId, items);
|
|
587
|
-
for (const item of items) this.designs.set(item.id, item);
|
|
588
774
|
return items;
|
|
589
775
|
}
|
|
590
776
|
/** The version rule. */
|
|
@@ -624,10 +810,10 @@ var DesignResolver = class {
|
|
|
624
810
|
};
|
|
625
811
|
}
|
|
626
812
|
/**
|
|
627
|
-
* The full design for one version.
|
|
813
|
+
* The full design for one version, from the single-version read.
|
|
628
814
|
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
815
|
+
* One request per version a session actually describes, not one per version in the list:
|
|
816
|
+
* an overview and any number of component slices of the same version share it.
|
|
631
817
|
*/
|
|
632
818
|
async design(architectureId) {
|
|
633
819
|
const cached = this.designs.get(architectureId);
|
|
@@ -722,7 +908,9 @@ var input = z2.object({
|
|
|
722
908
|
target: z2.string().max(255),
|
|
723
909
|
protocol: z2.string().max(64).optional()
|
|
724
910
|
})
|
|
725
|
-
).max(500).optional().describe(
|
|
911
|
+
).max(500).optional().describe(
|
|
912
|
+
"Edges in the code the design does not have, each from the caller to the callee: the component that holds the client is the source, including a consumer reading a queue. Endpoints may be component ids or names."
|
|
913
|
+
)
|
|
726
914
|
});
|
|
727
915
|
var refs = z2.array(z2.object({ id: z2.string(), name: z2.string(), type: z2.string() }));
|
|
728
916
|
var output = z2.object({
|
|
@@ -858,7 +1046,10 @@ var output2 = z3.object({
|
|
|
858
1046
|
category: z3.string(),
|
|
859
1047
|
message: z3.string(),
|
|
860
1048
|
affected_components: z3.array(z3.string()),
|
|
861
|
-
remediation: z3.string()
|
|
1049
|
+
remediation: z3.string(),
|
|
1050
|
+
advisory_only: z3.boolean().describe(
|
|
1051
|
+
"True for advice: reported and never charged against the score \u2014 a framework nobody named, or a check too uncertain to count. Not a problem to fix."
|
|
1052
|
+
)
|
|
862
1053
|
})
|
|
863
1054
|
),
|
|
864
1055
|
finding_count: z3.number(),
|
|
@@ -897,7 +1088,9 @@ function registerCritique(server, client) {
|
|
|
897
1088
|
category: finding.category,
|
|
898
1089
|
message: finding.message,
|
|
899
1090
|
affected_components: finding.affected_components ?? [],
|
|
900
|
-
remediation: finding.remediation ?? ""
|
|
1091
|
+
remediation: finding.remediation ?? "",
|
|
1092
|
+
// A server too old to send the flag predates it, and counted every finding.
|
|
1093
|
+
advisory_only: finding.advisory_only ?? false
|
|
901
1094
|
}));
|
|
902
1095
|
const complianceAssessed = critique.compliance_assessed ?? false;
|
|
903
1096
|
const exposureAssessed = critique.exposure_assessed ?? "public_internet";
|
|
@@ -931,8 +1124,12 @@ function registerCritique(server, client) {
|
|
|
931
1124
|
"",
|
|
932
1125
|
findings.length === 0 ? "No findings." : `${findings.length} finding(s):`,
|
|
933
1126
|
...findings.map(
|
|
934
|
-
(finding) =>
|
|
1127
|
+
(finding) => (
|
|
1128
|
+
// Said on the line, because a severity alone reads as a blocker: an
|
|
1129
|
+
// advisory critical is one the score and the app's verdict both ignore.
|
|
1130
|
+
`- [${finding.severity}${finding.advisory_only ? " \xB7 advisory, not scored" : ""}] ${finding.message}` + (finding.affected_components.length ? ` (${finding.affected_components.join(", ")})` : "") + (finding.remediation ? `
|
|
935
1131
|
Fix: ${finding.remediation}` : "")
|
|
1132
|
+
)
|
|
936
1133
|
)
|
|
937
1134
|
];
|
|
938
1135
|
return ok(structured, lines.join("\n"));
|
|
@@ -1048,8 +1245,12 @@ var constraintsInput = z4.strictObject({
|
|
|
1048
1245
|
preferred_cloud: z4.enum(["aws", "gcp", "azure", "any"]).optional(),
|
|
1049
1246
|
preferred_technologies: z4.array(z4.string()).optional(),
|
|
1050
1247
|
max_components: z4.number().int().positive().optional(),
|
|
1051
|
-
read_write_mix: z4.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe(
|
|
1052
|
-
|
|
1248
|
+
read_write_mix: z4.enum(["read_heavy", "balanced", "write_heavy"]).optional().describe(
|
|
1249
|
+
"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
|
+
),
|
|
1251
|
+
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 the tier default peak.'
|
|
1253
|
+
),
|
|
1053
1254
|
data_retention: z4.enum(["days", "months", "years", "indefinite"]).optional(),
|
|
1054
1255
|
consistency: z4.enum(["strict", "eventual", "mixed"]).optional(),
|
|
1055
1256
|
data_residency: z4.enum(["any", "us", "eu"]).optional()
|
|
@@ -1124,7 +1325,7 @@ function registerGenerate(server, client, resolver) {
|
|
|
1124
1325
|
"generate_architecture",
|
|
1125
1326
|
{
|
|
1126
1327
|
title: "Generate a SkeletIQ architecture",
|
|
1127
|
-
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, and the scale and constraints it runs under
|
|
1328
|
+
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, and the scale and constraints it runs under. A prompt that names no system to design comes back as clarification_required, and so does one that names a system but no scale to build it for \u2014 how many users or requests it has to serve. Answer with clarification_answers; there is no way to skip the questions, because a design built on an invented scale is sized, priced and judged against a number nobody chose.",
|
|
1128
1329
|
inputSchema: generateInput,
|
|
1129
1330
|
outputSchema: generateOutput,
|
|
1130
1331
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
@@ -1238,7 +1439,7 @@ function clarificationResult(questions) {
|
|
|
1238
1439
|
options: (question.options ?? []).filter((option) => !(question.decline_options ?? []).includes(option))
|
|
1239
1440
|
}));
|
|
1240
1441
|
const lines = [
|
|
1241
|
-
"SkeletIQ will not design from this prompt yet \u2014 it
|
|
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 scale it runs at. Nothing was generated and nothing was charged.",
|
|
1242
1443
|
"",
|
|
1243
1444
|
...structured.flatMap((question) => [
|
|
1244
1445
|
`${question.id}: ${question.question}`,
|
|
@@ -1369,32 +1570,83 @@ async function describeCreated(resolver, architectureId) {
|
|
|
1369
1570
|
import * as z5 from "zod/v4";
|
|
1370
1571
|
|
|
1371
1572
|
// src/lib/slice.ts
|
|
1372
|
-
function sliceComponent(design, componentId) {
|
|
1573
|
+
function sliceComponent(design, componentId, ledger) {
|
|
1373
1574
|
const components = design.components ?? [];
|
|
1374
1575
|
const component = components.find((c) => c.id === componentId) ?? components.find((c) => normalize(c.name) === normalize(componentId));
|
|
1375
1576
|
if (!component) return null;
|
|
1376
1577
|
const connections = design.connections ?? [];
|
|
1578
|
+
const related = relatedDecisions(design, component);
|
|
1377
1579
|
return {
|
|
1378
1580
|
component,
|
|
1379
1581
|
outgoing: connections.filter((c) => c.source === component.id),
|
|
1380
1582
|
incoming: connections.filter((c) => c.target === component.id),
|
|
1381
|
-
related_decisions:
|
|
1583
|
+
related_decisions: related,
|
|
1584
|
+
assumed_decisions: assumedAmong(design, related),
|
|
1585
|
+
...ledger ? {
|
|
1586
|
+
cited_requirements: citedRequirements(ledger, component.id),
|
|
1587
|
+
withdrawn_requirements: withdrawnRequirements(ledger, component.id)
|
|
1588
|
+
} : {}
|
|
1382
1589
|
};
|
|
1383
1590
|
}
|
|
1591
|
+
function citedRequirements(ledger, componentId) {
|
|
1592
|
+
return orderRequirementRows(
|
|
1593
|
+
(ledger.rows ?? []).filter((row) => (row.cited_by_components ?? []).includes(componentId))
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
function withdrawnRequirements(ledger, componentId) {
|
|
1597
|
+
return [...ledger.removed ?? [], ...ledger.superseded ?? []].filter(
|
|
1598
|
+
(row) => (row.cited_by_components ?? []).includes(componentId)
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
var VERDICT_ORDER = ["fails", "unmet", "not_checked", "not_checkable", "unchecked", "no_gap_found", "passes", "cited"];
|
|
1602
|
+
function isCitedRequirement(row) {
|
|
1603
|
+
return (row.cited_by_components?.length ?? 0) > 0 || (row.cited_by_decisions?.length ?? 0) > 0;
|
|
1604
|
+
}
|
|
1605
|
+
var CITATION_ONLY_METHOD = "structure";
|
|
1606
|
+
function requirementVerdict(row) {
|
|
1607
|
+
const verdict = (row.verdict ?? row.status ?? "").trim();
|
|
1608
|
+
if (verdict === "fails" && (row.check_method ?? "").trim() === CITATION_ONLY_METHOD) return "";
|
|
1609
|
+
return verdict;
|
|
1610
|
+
}
|
|
1611
|
+
function orderRequirementRows(rows) {
|
|
1612
|
+
return [...rows].sort((a, b) => verdictRank(a) - verdictRank(b));
|
|
1613
|
+
}
|
|
1614
|
+
function verdictRank(row) {
|
|
1615
|
+
const verdict = requirementVerdict(row);
|
|
1616
|
+
if (verdict === "fails" || verdict === "unmet") return 0;
|
|
1617
|
+
if (!isCitedRequirement(row)) return 1;
|
|
1618
|
+
const index = VERDICT_ORDER.indexOf(verdict);
|
|
1619
|
+
return index === -1 ? VERDICT_ORDER.length + 1 : index + 2;
|
|
1620
|
+
}
|
|
1384
1621
|
function decisionTexts(design) {
|
|
1385
|
-
|
|
1622
|
+
return decisionEntries(design).map((entry) => entry.text);
|
|
1623
|
+
}
|
|
1624
|
+
function decisionEntries(design) {
|
|
1625
|
+
const byText = /* @__PURE__ */ new Map();
|
|
1386
1626
|
const out = [];
|
|
1387
|
-
const push2 = (text) => {
|
|
1627
|
+
const push2 = (text, assumption) => {
|
|
1388
1628
|
if (typeof text !== "string") return;
|
|
1389
1629
|
const trimmed = text.trim();
|
|
1390
|
-
if (trimmed.length === 0
|
|
1391
|
-
|
|
1392
|
-
|
|
1630
|
+
if (trimmed.length === 0) return;
|
|
1631
|
+
const existing = byText.get(trimmed);
|
|
1632
|
+
if (existing) {
|
|
1633
|
+
existing.assumption = existing.assumption || assumption;
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const entry = { text: trimmed, assumption };
|
|
1637
|
+
byText.set(trimmed, entry);
|
|
1638
|
+
out.push(entry);
|
|
1393
1639
|
};
|
|
1394
|
-
for (const decision of design.design_decisions ?? []) push2(decision);
|
|
1395
|
-
for (const decision of design.grounded_decisions ?? []) push2(decision?.text);
|
|
1640
|
+
for (const decision of design.design_decisions ?? []) push2(decision, false);
|
|
1641
|
+
for (const decision of design.grounded_decisions ?? []) push2(decision?.text, decision?.assumption === true);
|
|
1396
1642
|
return out;
|
|
1397
1643
|
}
|
|
1644
|
+
function assumedAmong(design, texts) {
|
|
1645
|
+
const assumed = new Set(
|
|
1646
|
+
decisionEntries(design).filter((entry) => entry.assumption).map((entry) => entry.text)
|
|
1647
|
+
);
|
|
1648
|
+
return texts.filter((text) => assumed.has(text));
|
|
1649
|
+
}
|
|
1398
1650
|
function relatedDecisions(design, component) {
|
|
1399
1651
|
const needles = [component.name, component.technology].filter((value) => typeof value === "string" && value.trim().length > 2).map((value) => value.toLowerCase());
|
|
1400
1652
|
if (needles.length === 0) return [];
|
|
@@ -1403,14 +1655,24 @@ function relatedDecisions(design, component) {
|
|
|
1403
1655
|
return needles.some((needle) => haystack.includes(needle));
|
|
1404
1656
|
});
|
|
1405
1657
|
}
|
|
1406
|
-
function overview(design) {
|
|
1658
|
+
function overview(design, ledger) {
|
|
1659
|
+
const decisions = decisionTexts(design);
|
|
1407
1660
|
return {
|
|
1408
1661
|
title: design.title ?? "Untitled design",
|
|
1409
1662
|
description: design.description ?? "",
|
|
1410
1663
|
components: design.components ?? [],
|
|
1411
1664
|
connections: design.connections ?? [],
|
|
1412
|
-
design_decisions:
|
|
1413
|
-
|
|
1665
|
+
design_decisions: decisions,
|
|
1666
|
+
// A subset of `design_decisions`, not a second list of decisions: which of them the design
|
|
1667
|
+
// marks as resting on no cited source. Additive, so `design_decisions` stays the plain
|
|
1668
|
+
// strings every existing caller reads.
|
|
1669
|
+
assumed_decisions: assumedAmong(design, decisions),
|
|
1670
|
+
trade_offs: design.trade_offs ?? [],
|
|
1671
|
+
// Carried verbatim, rows in the server's order, and only when the version has one. This is
|
|
1672
|
+
// a rebuild from named keys, so anything not named here is dropped without a trace — which
|
|
1673
|
+
// is how `grounded_decisions` went missing for two releases. Re-sorting the rows on the way
|
|
1674
|
+
// past would make "verbatim" untrue; the grouping belongs to the text that prints them.
|
|
1675
|
+
...ledger ? { requirement_ledger: ledger } : {}
|
|
1414
1676
|
};
|
|
1415
1677
|
}
|
|
1416
1678
|
function normalize(value) {
|
|
@@ -1418,11 +1680,11 @@ function normalize(value) {
|
|
|
1418
1680
|
}
|
|
1419
1681
|
|
|
1420
1682
|
// src/tools/get-design.ts
|
|
1421
|
-
var MODES = ["overview", "component", "brief", "readiness", "build_order", "gaps"];
|
|
1683
|
+
var MODES = ["overview", "component", "brief", "todo", "readiness", "build_order", "gaps"];
|
|
1422
1684
|
var input3 = z5.object({
|
|
1423
1685
|
project_id: z5.string().describe("A SkeletIQ project id, or its exact name."),
|
|
1424
1686
|
mode: z5.enum(MODES).describe(
|
|
1425
|
-
"overview: the whole design. component: one component, its connections in both directions
|
|
1687
|
+
"overview: the whole design and, where the version records them, the requirements it was built from with what became of each. component: one component, its connections in both directions (the source calls the target), the decisions that mention it, and the requirements the design cites it for. brief: the fenced markdown block to write into AGENTS.md. todo: everything still to do on this version, one card per problem \u2014 what blocks a handoff, what is recommended, and what is waiting to be accepted. readiness: the verdict, gate by gate, on whether this design is worth building yet. build_order: the order to build components in, and why. gaps: the open questions and unconfirmed assumptions in the design."
|
|
1426
1688
|
),
|
|
1427
1689
|
component_id: z5.string().optional().describe('Required for mode "component". The component id, or its name.'),
|
|
1428
1690
|
version: z5.number().int().positive().optional().describe("A specific version. Defaults to the latest released version, or the latest version if none is released.")
|
|
@@ -1438,12 +1700,19 @@ var output3 = z5.object({
|
|
|
1438
1700
|
/** Mode-shaped. Deliberately loose: six modes cannot share one strict schema honestly. */
|
|
1439
1701
|
data: z5.unknown()
|
|
1440
1702
|
});
|
|
1703
|
+
function decisionsHeading(assumed, settled, withAssumptions) {
|
|
1704
|
+
return assumed.length > 0 ? withAssumptions : settled;
|
|
1705
|
+
}
|
|
1706
|
+
function decisionLines(decisions, assumed) {
|
|
1707
|
+
const marked = new Set(assumed);
|
|
1708
|
+
return decisions.map((decision) => `- ${decision}${marked.has(decision) ? " (assumption)" : ""}`);
|
|
1709
|
+
}
|
|
1441
1710
|
function registerGetDesign(server, client, resolver) {
|
|
1442
1711
|
server.registerTool(
|
|
1443
1712
|
"get_design",
|
|
1444
1713
|
{
|
|
1445
1714
|
title: "Read a SkeletIQ design",
|
|
1446
|
-
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.',
|
|
1715
|
+
description: 'Read a SkeletIQ architecture. Start with mode "brief" to orient \u2014 it returns a fenced block to write into AGENTS.md \u2014 then "todo" for what the design still needs, "build_order" for the sequence and "component" for each piece as you build it. Where a version records its requirements, "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.',
|
|
1447
1716
|
inputSchema: input3,
|
|
1448
1717
|
outputSchema: output3,
|
|
1449
1718
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
|
|
@@ -1459,7 +1728,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1459
1728
|
switch (mode) {
|
|
1460
1729
|
case "overview": {
|
|
1461
1730
|
const design = await resolver.design(resolved.architectureId);
|
|
1462
|
-
const data = overview(design.architecture_json);
|
|
1731
|
+
const data = overview(design.architecture_json, design.requirement_ledger);
|
|
1463
1732
|
const facts = factsFrom(resolved);
|
|
1464
1733
|
return ok({ ...base, ...facts, data }, [
|
|
1465
1734
|
`# ${data.title}`,
|
|
@@ -1469,6 +1738,10 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1469
1738
|
...data.components.map(
|
|
1470
1739
|
(c) => `- ${c.id} \u2014 ${c.name} (${c.type}${c.technology ? `, ${c.technology}` : ""})`
|
|
1471
1740
|
),
|
|
1741
|
+
// Above the decisions because the two answer different questions, in
|
|
1742
|
+
// this order: requirements are what was asked for, decisions are how
|
|
1743
|
+
// the design answered.
|
|
1744
|
+
...requirementBlock(data.requirement_ledger),
|
|
1472
1745
|
// The design's own reasoning, which this text used to omit entirely
|
|
1473
1746
|
// while carrying it in the structured half — so an agent working from
|
|
1474
1747
|
// what it was shown rebuilt every choice from scratch, and could
|
|
@@ -1476,8 +1749,12 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1476
1749
|
// like this", and the overview is where that gets asked.
|
|
1477
1750
|
...data.design_decisions.length ? [
|
|
1478
1751
|
"",
|
|
1479
|
-
|
|
1480
|
-
|
|
1752
|
+
decisionsHeading(
|
|
1753
|
+
data.assumed_decisions,
|
|
1754
|
+
"Decisions taken \u2014 these are settled, build to them:",
|
|
1755
|
+
"Decisions taken \u2014 build to them; confirm the ones marked (assumption) first, they rest on no cited source:"
|
|
1756
|
+
),
|
|
1757
|
+
...decisionLines(data.design_decisions, data.assumed_decisions)
|
|
1481
1758
|
] : [],
|
|
1482
1759
|
...data.trade_offs.length ? [
|
|
1483
1760
|
"",
|
|
@@ -1493,7 +1770,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1493
1770
|
return failure('mode "component" needs a component_id. Use mode "overview" or "build_order" to see the ids.');
|
|
1494
1771
|
}
|
|
1495
1772
|
const design = await resolver.design(resolved.architectureId);
|
|
1496
|
-
const slice = sliceComponent(design.architecture_json, component_id);
|
|
1773
|
+
const slice = sliceComponent(design.architecture_json, component_id, design.requirement_ledger);
|
|
1497
1774
|
if (!slice) {
|
|
1498
1775
|
const known = (design.architecture_json.components ?? []).map((c) => c.id).join(", ");
|
|
1499
1776
|
return failure(
|
|
@@ -1509,6 +1786,14 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1509
1786
|
"",
|
|
1510
1787
|
outgoing.length ? `Calls: ${outgoing.map((c) => `${c.target}${c.protocol ? ` (${c.protocol})` : ""}`).join(", ")}` : "Calls nothing.",
|
|
1511
1788
|
incoming.length ? `Called by: ${incoming.map((c) => c.source).join(", ")}` : "Called by nothing in the design.",
|
|
1789
|
+
// Same order as the overview: what was asked for, then how the design
|
|
1790
|
+
// answered.
|
|
1791
|
+
...requirementsServed(slice.cited_requirements),
|
|
1792
|
+
// Said only when the design still carries the id. A component's
|
|
1793
|
+
// `requirement_ids` are never rewritten when a requirement is
|
|
1794
|
+
// withdrawn, so without this the row would leave the answer in
|
|
1795
|
+
// silence and the id on the box would resolve to nothing again.
|
|
1796
|
+
...requirementsWithdrawn(slice.withdrawn_requirements),
|
|
1512
1797
|
// Labelled, and only when there are any. These bullets used to be
|
|
1513
1798
|
// printed bare under "Called by:", so a decision about the component
|
|
1514
1799
|
// read as a continuation of its edge list — and a design with no
|
|
@@ -1516,8 +1801,12 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1516
1801
|
// with no reasoning rather than as a decision that names other boxes.
|
|
1517
1802
|
...slice.related_decisions.length ? [
|
|
1518
1803
|
"",
|
|
1519
|
-
|
|
1520
|
-
|
|
1804
|
+
decisionsHeading(
|
|
1805
|
+
slice.assumed_decisions,
|
|
1806
|
+
"Decisions that mention it:",
|
|
1807
|
+
"Decisions that mention it \u2014 confirm the ones marked (assumption) first, they rest on no cited source:"
|
|
1808
|
+
),
|
|
1809
|
+
...decisionLines(slice.related_decisions, slice.assumed_decisions)
|
|
1521
1810
|
] : [
|
|
1522
1811
|
"",
|
|
1523
1812
|
"No decision in this design names this component. That is not the same as",
|
|
@@ -1551,6 +1840,33 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1551
1840
|
versionLine(facts)
|
|
1552
1841
|
].filter(Boolean).join("\n"));
|
|
1553
1842
|
}
|
|
1843
|
+
case "todo": {
|
|
1844
|
+
const body = await client.request(`/architectures/${resolved.architectureId}/todo`);
|
|
1845
|
+
const todo = TodoSchema.parse(body);
|
|
1846
|
+
const facts = factsFrom(resolved);
|
|
1847
|
+
const groups = todo.groups ?? [];
|
|
1848
|
+
const blocking = groups.find((group) => group.key === "blocks_handoff");
|
|
1849
|
+
const total = groups.reduce((count, group) => count + group.items.length, 0);
|
|
1850
|
+
return ok({ ...base, ...facts, data: todo }, [
|
|
1851
|
+
total === 0 ? "Nothing is outstanding on this version." : `${total} thing(s) still to do on this version` + (blocking?.items.length ? `, ${blocking.items.length} of which block a handoff:` : ", none of which block a handoff:"),
|
|
1852
|
+
...groups.flatMap(todoGroupBlock),
|
|
1853
|
+
// A source that could not be read is stated, never left to look like a
|
|
1854
|
+
// shorter list. An agent acting on "nothing is outstanding" when the
|
|
1855
|
+
// simulation results failed to load is the exact harm this prevents.
|
|
1856
|
+
...todo.unavailable_sources?.length ? [
|
|
1857
|
+
"",
|
|
1858
|
+
"Some of this version could not be read, so the list above is incomplete:",
|
|
1859
|
+
...todo.unavailable_sources.map((entry) => `- ${entry.source}: ${entry.reason}`)
|
|
1860
|
+
] : [],
|
|
1861
|
+
"",
|
|
1862
|
+
// The actions are the person's. An agent that "accepts" an assumption
|
|
1863
|
+
// has decided something on their behalf, which is the one thing this
|
|
1864
|
+
// surface must never do — and the server refuses it anyway, because an
|
|
1865
|
+
// agent token reaches no write route.
|
|
1866
|
+
"Fix what you can in the design. Answering, accepting and dismissing are the person's to do \u2014 ask, do not decide for them.",
|
|
1867
|
+
versionLine(facts)
|
|
1868
|
+
].join("\n"));
|
|
1869
|
+
}
|
|
1554
1870
|
case "readiness": {
|
|
1555
1871
|
const body = await client.request(`/architectures/${resolved.architectureId}/readiness`);
|
|
1556
1872
|
const readiness = ReadinessSchema.parse(body);
|
|
@@ -1558,7 +1874,10 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1558
1874
|
const gates = readiness.rows.filter((row) => row.category === "gate");
|
|
1559
1875
|
const open = gates.filter((row) => row.gating);
|
|
1560
1876
|
const unanswered = gates.filter((row) => row.state === "unknown");
|
|
1561
|
-
const
|
|
1877
|
+
const accepts = readiness.rows.filter((row) => row.category === "accept");
|
|
1878
|
+
const advisories = readiness.rows.filter(
|
|
1879
|
+
(row) => row.category !== "gate" && row.category !== "accept"
|
|
1880
|
+
);
|
|
1562
1881
|
const unrun = new Set(readiness.unrun_checks ?? []);
|
|
1563
1882
|
const verdict = readiness.verdict ?? (readiness.ready ? "ready" : "outstanding");
|
|
1564
1883
|
const headline = verdict === "ready" ? "Every gate is clear \u2014 this design is ready to build." : verdict === "unanswered" ? `Nothing is undecided, but ${unanswered.length} check(s) could not be answered for this version:` : `${open.length} of ${gates.length} gate(s) still open:`;
|
|
@@ -1566,13 +1885,22 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1566
1885
|
return ok({ ...base, ...facts, data: readiness }, [
|
|
1567
1886
|
headline,
|
|
1568
1887
|
...listed.map((row) => `- ${row.label}: ${describeRow(row)}`),
|
|
1888
|
+
...accepts.length ? [
|
|
1889
|
+
"",
|
|
1890
|
+
"To accept (the person agrees to these rather than fixing them; accepting is free and never blocks):",
|
|
1891
|
+
...accepts.map((row) => `- ${row.label}: ${describeRow(row)}`)
|
|
1892
|
+
] : [],
|
|
1569
1893
|
"",
|
|
1570
1894
|
"Advisory checks (they never block, and some need a paid plan to clear):",
|
|
1571
|
-
// An advisory
|
|
1572
|
-
//
|
|
1573
|
-
//
|
|
1895
|
+
// An advisory with no result is marked, not counted — "nothing to count
|
|
1896
|
+
// here" is a fact an agent being handed work is entitled to, and it is
|
|
1897
|
+
// invisible in a bare count of open items. The mark claims no more than
|
|
1898
|
+
// that: an advisory can be unanswered because no result is *stored* for
|
|
1899
|
+
// this version (the advisor row since the server stopped writing on
|
|
1900
|
+
// read), not only because nothing ever ran, and the server's own
|
|
1901
|
+
// sentence, when it sends one, says which.
|
|
1574
1902
|
...advisories.map(
|
|
1575
|
-
(row) => `- ${row.label}: ${describeRow(row)}${unrun.has(row.key) ? " (
|
|
1903
|
+
(row) => `- ${row.label}: ${describeRow(row)}${unrun.has(row.key) && !row.detail ? " (no result for this version)" : ""}`
|
|
1576
1904
|
),
|
|
1577
1905
|
// The same list, under the same heading, as the app shows beside its
|
|
1578
1906
|
// Release button. A person clicking Release is told what the release
|
|
@@ -1585,7 +1913,7 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1585
1913
|
...readiness.release_warnings.map((warning) => `- ${warning}`)
|
|
1586
1914
|
] : [],
|
|
1587
1915
|
"",
|
|
1588
|
-
|
|
1916
|
+
'Only a person can clear a gate. Ask; do not decide on their behalf. Use mode "todo" for the same work item by item.',
|
|
1589
1917
|
versionLine(facts)
|
|
1590
1918
|
].join("\n"));
|
|
1591
1919
|
}
|
|
@@ -1644,6 +1972,70 @@ function registerGetDesign(server, client, resolver) {
|
|
|
1644
1972
|
})
|
|
1645
1973
|
);
|
|
1646
1974
|
}
|
|
1975
|
+
function requirementBlock(ledger) {
|
|
1976
|
+
const rows = orderRequirementRows(ledger?.rows ?? []);
|
|
1977
|
+
if (!ledger || rows.length === 0) return [];
|
|
1978
|
+
const attention = [
|
|
1979
|
+
[rows.filter((row) => requirementVerdict(row) === "fails").length, "failing"],
|
|
1980
|
+
[rows.filter((row) => !isCitedRequirement(row)).length, "cited by nothing"],
|
|
1981
|
+
[rows.filter((row) => requirementVerdict(row) === "not_checked" || requirementVerdict(row) === "not_checkable").length, "unchecked"]
|
|
1982
|
+
].filter(([count]) => count > 0).map(([count, word]) => `${count} ${word}`);
|
|
1983
|
+
return [
|
|
1984
|
+
"",
|
|
1985
|
+
`Requirements \u2014 ${rows.length} total${attention.length ? `; ${attention.join(", ")}` : ""}:`,
|
|
1986
|
+
...rows.map(requirementLine),
|
|
1987
|
+
"Verdicts: `fails` \u2014 a named check measured the design as not meeting it, and says which and why;",
|
|
1988
|
+
"`passes` \u2014 a check measured it against a figure the person stated; `no_gap_found` \u2014 nothing",
|
|
1989
|
+
"contradicted it, which is not the same as proven; `not_checked` / `not_checkable` \u2014 nobody",
|
|
1990
|
+
"established either way, and neither is fine. `cited by nothing` means no component and no",
|
|
1991
|
+
"decision in the design claims to address it.",
|
|
1992
|
+
// Two facts about how far the statuses can be trusted, said only when the server states
|
|
1993
|
+
// them. An absent `verification_state` is an older server, not a verifier that did not run,
|
|
1994
|
+
// and claiming the second would be this package inventing a fact about someone's design.
|
|
1995
|
+
...ledger.grounding_state === "not_grounded" ? [
|
|
1996
|
+
"This version records no citations at all, so `uncited` here means nothing recorded what",
|
|
1997
|
+
"addresses the requirement \u2014 not that nothing in the design does."
|
|
1998
|
+
] : [],
|
|
1999
|
+
...ledger.verification_state === "partial" ? [
|
|
2000
|
+
"The verifier did not check every requirement in this set, so an `unchecked` row is",
|
|
2001
|
+
"neither confirmed nor flagged."
|
|
2002
|
+
] : ledger.verification_state === "not_run" ? [
|
|
2003
|
+
"The verifier did not run on this version, so an `unchecked` row is neither confirmed",
|
|
2004
|
+
"nor flagged."
|
|
2005
|
+
] : [],
|
|
2006
|
+
...ledger.analysis_degraded ? ["The requirements analysis for this version ran degraded. Treat the list as incomplete."] : []
|
|
2007
|
+
];
|
|
2008
|
+
}
|
|
2009
|
+
function requirementsServed(rows) {
|
|
2010
|
+
if (!rows?.length) return [];
|
|
2011
|
+
return [
|
|
2012
|
+
"",
|
|
2013
|
+
"Requirements the design cites this component for:",
|
|
2014
|
+
...rows.map(requirementLine),
|
|
2015
|
+
"`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."
|
|
2017
|
+
];
|
|
2018
|
+
}
|
|
2019
|
+
function requirementsWithdrawn(rows) {
|
|
2020
|
+
if (!rows?.length) return [];
|
|
2021
|
+
return [
|
|
2022
|
+
"",
|
|
2023
|
+
"This component still cites these ids, and they are no longer requirements \u2014 do not build to them:",
|
|
2024
|
+
...rows.map(
|
|
2025
|
+
(row) => `- ${row.id} ${(row.text ?? "").replace(/\s+/g, " ").trim() || "(no text recorded)"} [${row.decision === "removed" ? "removed by a person" : `was an instruction, now ${row.superseded_by_change_request ?? "a change request"}`}]`
|
|
2026
|
+
)
|
|
2027
|
+
];
|
|
2028
|
+
}
|
|
2029
|
+
function requirementLine(row) {
|
|
2030
|
+
const verdict = requirementVerdict(row);
|
|
2031
|
+
const marks = [verdict, ...isCitedRequirement(row) ? [] : ["cited by nothing"]].filter(Boolean);
|
|
2032
|
+
const label = marks.length > 0 ? ` [${marks.join(" \xB7 ")}]` : "";
|
|
2033
|
+
const text = (row.text ?? "").replace(/\s+/g, " ").trim();
|
|
2034
|
+
const reason = (row.verdict_reason ?? "").replace(/\s+/g, " ").trim();
|
|
2035
|
+
const by = (row.checked_by ?? "").trim();
|
|
2036
|
+
const because = reason ? ` \u2014 ${reason}${by ? ` (checked by ${by})` : ""}` : by ? ` \u2014 checked by ${by}` : "";
|
|
2037
|
+
return `- ${row.id}${label} ${text || "(no text recorded for this requirement)"}${because}`;
|
|
2038
|
+
}
|
|
1647
2039
|
function settledAnswer(gap) {
|
|
1648
2040
|
const note = (gap.note ?? "").trim();
|
|
1649
2041
|
if (gap.adr_id) {
|
|
@@ -1654,8 +2046,20 @@ function settledAnswer(gap) {
|
|
|
1654
2046
|
if (gap.action === "confirmed") return "confirmed as it stands";
|
|
1655
2047
|
return "answered, but no wording was recorded \u2014 ask before relying on it";
|
|
1656
2048
|
}
|
|
2049
|
+
function todoGroupBlock(group) {
|
|
2050
|
+
if (group.items.length === 0) return [];
|
|
2051
|
+
return ["", `${group.label} (${group.items.length}):`, ...group.items.map(todoLine)];
|
|
2052
|
+
}
|
|
2053
|
+
function todoLine(item) {
|
|
2054
|
+
const severity = item.severity && item.severity !== "none" ? `[${item.severity}] ` : "";
|
|
2055
|
+
const raisedBy = [...new Set((item.sources ?? []).map((source) => source.source))];
|
|
2056
|
+
const raised = raisedBy.length > 0 ? ` \u2014 raised by ${raisedBy.join(", ")}` : "";
|
|
2057
|
+
const carried = item.carried ? item.carried_from_version ? ` \u2014 carried from v${item.carried_from_version}` : " \u2014 carried from an earlier version" : "";
|
|
2058
|
+
const components = item.components?.length ? ` \u2014 affects ${item.components.join(", ")}` : "";
|
|
2059
|
+
return `- ${severity}${item.title.replace(/\s+/g, " ").trim()}${components}${raised}${carried}`;
|
|
2060
|
+
}
|
|
1657
2061
|
function describeRow(row) {
|
|
1658
|
-
if (row.state === "unknown") return
|
|
2062
|
+
if (row.state === "unknown") return `not checked${row.detail ? ` \u2014 ${row.detail}` : ""}`;
|
|
1659
2063
|
return `${row.count}${row.detail ? ` \u2014 ${row.detail}` : ""}`;
|
|
1660
2064
|
}
|
|
1661
2065
|
|
package/package.json
CHANGED