@wairon/cli 5.1.1-dev.68 → 5.1.1-dev.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +167 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +152 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -65,7 +65,7 @@ var init_defaults = __esm({
|
|
|
65
65
|
copilot: ".github/prompts",
|
|
66
66
|
codex: ".codex/agents"
|
|
67
67
|
};
|
|
68
|
-
WAIRON_VERSION = "5.1.1-dev.
|
|
68
|
+
WAIRON_VERSION = "5.1.1-dev.69";
|
|
69
69
|
GITHUB_REPO = "SYW-Apps/Waffle-AIron";
|
|
70
70
|
SUPPORTED_ALIASES = ["wai"];
|
|
71
71
|
SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -1770,7 +1770,7 @@ function typeSourceFiles(type) {
|
|
|
1770
1770
|
for (const method2 of type.methods ?? []) add(method2.sourcePath);
|
|
1771
1771
|
return files;
|
|
1772
1772
|
}
|
|
1773
|
-
var import_zod8, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, RETIRED_STEREOTYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, DependencyClassSchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, ComponentSpecSchema, BLOCK_NOUNS, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, FindingDeclarationSchema, MethodSignatureSchema, INTENT_FLOOR_MIN_CHARS, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
|
|
1773
|
+
var import_zod8, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, RETIRED_STEREOTYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, DependencyClassSchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, PortalMountSchema, ComponentSpecSchema, BLOCK_NOUNS, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, FindingDeclarationSchema, MethodSignatureSchema, INTENT_FLOOR_MIN_CHARS, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
|
|
1774
1774
|
var init_specs = __esm({
|
|
1775
1775
|
"src/models/specs.ts"() {
|
|
1776
1776
|
"use strict";
|
|
@@ -2024,6 +2024,24 @@ var init_specs = __esm({
|
|
|
2024
2024
|
description: import_zod8.z.string().optional(),
|
|
2025
2025
|
example: import_zod8.z.string().optional()
|
|
2026
2026
|
});
|
|
2027
|
+
PortalMountSchema = import_zod8.z.object({
|
|
2028
|
+
/** The mounted Portal's component id. */
|
|
2029
|
+
portal: import_zod8.z.string(),
|
|
2030
|
+
/**
|
|
2031
|
+
* The path prefixes this listener routes to the portal. A path lies under a
|
|
2032
|
+
* prefix when it EQUALS it or continues it past a slash — so `/` covers the
|
|
2033
|
+
* root itself and nothing else, which is what lets an app shell sit at `GET /`
|
|
2034
|
+
* without swallowing every other route.
|
|
2035
|
+
*/
|
|
2036
|
+
prefixes: import_zod8.z.array(import_zod8.z.string()),
|
|
2037
|
+
/**
|
|
2038
|
+
* The router entry the listener calls to hand the portal its request,
|
|
2039
|
+
* exported by the PORTAL's own file (so export-conformance holds that file to
|
|
2040
|
+
* it). Absent when the listener calls the portal's contract methods directly,
|
|
2041
|
+
* route by route — then there is no entry to name.
|
|
2042
|
+
*/
|
|
2043
|
+
via: import_zod8.z.string().optional()
|
|
2044
|
+
});
|
|
2027
2045
|
ComponentSpecSchema = import_zod8.z.object({
|
|
2028
2046
|
id: SpecIdSchema,
|
|
2029
2047
|
name: import_zod8.z.string(),
|
|
@@ -2043,6 +2061,13 @@ var init_specs = __esm({
|
|
|
2043
2061
|
auth: PortalAuthSchema.optional(),
|
|
2044
2062
|
/** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
|
|
2045
2063
|
dispatch: import_zod8.z.array(DispatchBindingSchema).optional(),
|
|
2064
|
+
/**
|
|
2065
|
+
* Portal-only: the portals this LISTENER serves (see PortalMountSchema).
|
|
2066
|
+
* Declaring the field — even as an empty array — is what marks a portal as a
|
|
2067
|
+
* listener: the one kind of portal the host starts directly, so nothing else
|
|
2068
|
+
* needs to mount it. Absent means "a portal something else must serve".
|
|
2069
|
+
*/
|
|
2070
|
+
mounts: import_zod8.z.array(PortalMountSchema).optional(),
|
|
2046
2071
|
/** Store-only: whether held state survives restart (see DurabilitySchema). */
|
|
2047
2072
|
durability: DurabilitySchema.optional(),
|
|
2048
2073
|
/** Orchestrator-only: what the logic may depend on; unset means a workflow (see DependencyClassSchema). */
|
|
@@ -12365,6 +12390,90 @@ var init_lifecycle_entrypoints = __esm({
|
|
|
12365
12390
|
}
|
|
12366
12391
|
});
|
|
12367
12392
|
|
|
12393
|
+
// src/core/rules/wiring/portal-mounts.ts
|
|
12394
|
+
function liesUnder(path71, prefix) {
|
|
12395
|
+
return path71 === prefix || path71.startsWith(prefix + "/");
|
|
12396
|
+
}
|
|
12397
|
+
var portalMountsRule;
|
|
12398
|
+
var init_portal_mounts = __esm({
|
|
12399
|
+
"src/core/rules/wiring/portal-mounts.ts"() {
|
|
12400
|
+
"use strict";
|
|
12401
|
+
portalMountsRule = {
|
|
12402
|
+
name: "portal-mounts",
|
|
12403
|
+
description: "Which listener serves which portal: a portal's routes are its methods' endpoint bindings, but nothing said which listener hands it its requests, so a portal no listener serves was unreachable without anyone noticing, and the call that mounts one crossed a boundary no contract described. A listener Portal declares its mounts \u2014 each a portal, the path prefixes routed to it, and optionally the router entry it calls \u2014 and this rule checks them against the tree: a mount must name a Portal, every HTTP endpoint of a mounted portal must lie under one of its prefixes, and every portal with HTTP endpoints must be mounted by some listener unless it is one. A path lies under a prefix when it equals it or continues it past a slash, so `/` covers only the root. Whether the named entry is really exported is the code's question, and export-conformance asks it.",
|
|
12404
|
+
codes: [
|
|
12405
|
+
{
|
|
12406
|
+
code: "MOUNT_TARGET_NOT_PORTAL",
|
|
12407
|
+
defaultSeverity: "error",
|
|
12408
|
+
summary: "A listener mounts something that is not a Portal \u2014 an unknown component, or one whose type cannot serve routes"
|
|
12409
|
+
},
|
|
12410
|
+
{
|
|
12411
|
+
code: "ENDPOINT_OUTSIDE_MOUNT",
|
|
12412
|
+
defaultSeverity: "warning",
|
|
12413
|
+
summary: "A mounted portal declares an HTTP endpoint under none of the prefixes its listener routes to it \u2014 a route the contract promises that no request can reach"
|
|
12414
|
+
},
|
|
12415
|
+
{
|
|
12416
|
+
code: "UNMOUNTED_PORTAL",
|
|
12417
|
+
defaultSeverity: "warning",
|
|
12418
|
+
summary: "A portal declares HTTP endpoints but no listener mounts it and it declares no mounts of its own \u2014 a whole surface nothing serves"
|
|
12419
|
+
}
|
|
12420
|
+
],
|
|
12421
|
+
check(ctx) {
|
|
12422
|
+
const httpRoutesOf = (compId) => {
|
|
12423
|
+
const routes = [];
|
|
12424
|
+
for (const method2 of ctx.interfaceMethodsOf(compId)) {
|
|
12425
|
+
const ep = method2.endpoint;
|
|
12426
|
+
if (ep?.transport === "HTTP") routes.push({ method: method2.name, verb: ep.method, path: ep.path });
|
|
12427
|
+
}
|
|
12428
|
+
return routes;
|
|
12429
|
+
};
|
|
12430
|
+
const mounted = /* @__PURE__ */ new Set();
|
|
12431
|
+
for (const listener of ctx.components) {
|
|
12432
|
+
if (listener.componentType !== "Portal" || !listener.mounts) continue;
|
|
12433
|
+
const listenerDraft = ctx.isComponentDraft(listener.id);
|
|
12434
|
+
for (const mount of listener.mounts) {
|
|
12435
|
+
const target = ctx.componentMap.get(mount.portal);
|
|
12436
|
+
if (!target || target.componentType !== "Portal") {
|
|
12437
|
+
ctx.addIssue(
|
|
12438
|
+
"error",
|
|
12439
|
+
"MOUNT_TARGET_NOT_PORTAL",
|
|
12440
|
+
target ? `Listener "${listener.id}" mounts "${mount.portal}" under ${mount.prefixes.map((p) => `"${p}"`).join(", ") || "no prefixes"}, but "${target.id}" is a ${target.componentType} \u2014 only a Portal serves routes, so this mount routes requests to nothing.` : `Listener "${listener.id}" mounts "${mount.portal}" under ${mount.prefixes.map((p) => `"${p}"`).join(", ") || "no prefixes"}, but no component "${mount.portal}" exists \u2014 this mount routes requests to nothing.`,
|
|
12441
|
+
listener.id,
|
|
12442
|
+
listenerDraft || (target ? ctx.isComponentDraft(target.id) : false)
|
|
12443
|
+
);
|
|
12444
|
+
continue;
|
|
12445
|
+
}
|
|
12446
|
+
mounted.add(target.id);
|
|
12447
|
+
for (const route of httpRoutesOf(target.id)) {
|
|
12448
|
+
if (mount.prefixes.some((prefix) => liesUnder(route.path, prefix))) continue;
|
|
12449
|
+
ctx.addIssue(
|
|
12450
|
+
"warning",
|
|
12451
|
+
"ENDPOINT_OUTSIDE_MOUNT",
|
|
12452
|
+
`Portal "${target.id}" binds "${route.method}" to ${route.verb} ${route.path}, but listener "${listener.id}" routes it only ${mount.prefixes.map((p) => `"${p}"`).join(", ") || "no prefixes at all"} \u2014 a route the contract promises that no request through this listener can reach. A path lies under a prefix when it equals it or continues it past a slash. Add the prefix to the mount, or move the endpoint under one it already routes.`,
|
|
12453
|
+
listener.id,
|
|
12454
|
+
listenerDraft || ctx.isComponentDraft(target.id)
|
|
12455
|
+
);
|
|
12456
|
+
}
|
|
12457
|
+
}
|
|
12458
|
+
}
|
|
12459
|
+
for (const portal of ctx.components) {
|
|
12460
|
+
if (portal.componentType !== "Portal") continue;
|
|
12461
|
+
if (portal.mounts !== void 0 || mounted.has(portal.id)) continue;
|
|
12462
|
+
const routes = httpRoutesOf(portal.id);
|
|
12463
|
+
if (routes.length === 0) continue;
|
|
12464
|
+
ctx.addIssue(
|
|
12465
|
+
"warning",
|
|
12466
|
+
"UNMOUNTED_PORTAL",
|
|
12467
|
+
`Portal "${portal.id}" declares ${routes.length} HTTP endpoint(s), but no listener mounts it and it declares no mounts of its own \u2014 a whole surface nothing serves. Mount it on the listener that routes its requests, or, if it IS a listener the host starts directly, declare its mounts (an empty list says it serves only its own routes).`,
|
|
12468
|
+
portal.id,
|
|
12469
|
+
ctx.isComponentDraft(portal.id)
|
|
12470
|
+
);
|
|
12471
|
+
}
|
|
12472
|
+
}
|
|
12473
|
+
};
|
|
12474
|
+
}
|
|
12475
|
+
});
|
|
12476
|
+
|
|
12368
12477
|
// src/core/rules/narrative-graph-projector.ts
|
|
12369
12478
|
function stepEdges(step, ctx, followRegisterEdges) {
|
|
12370
12479
|
if (step.type === "call" && step.targetComponent && step.targetMethod) {
|
|
@@ -14327,7 +14436,7 @@ var init_export_conformance = __esm({
|
|
|
14327
14436
|
NOTHING_PROMISED = /* @__PURE__ */ new Set();
|
|
14328
14437
|
exportConformanceRule = {
|
|
14329
14438
|
name: "export-conformance",
|
|
14330
|
-
description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code \u2014 it asks whether the code holds what a spec claims \u2014 so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it \u2014 the value that composes it, which `symbol` cannot name because `symbol` names the function inside \u2014 and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module.",
|
|
14439
|
+
description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code \u2014 it asks whether the code holds what a spec claims \u2014 so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it \u2014 the value that composes it, which `symbol` cannot name because `symbol` names the function inside \u2014 and a listener's mount names the router entry it calls to hand a portal its requests; both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module.",
|
|
14331
14440
|
codes: [
|
|
14332
14441
|
{
|
|
14333
14442
|
code: "UNDECLARED_EXPORT",
|
|
@@ -14340,7 +14449,7 @@ var init_export_conformance = __esm({
|
|
|
14340
14449
|
{
|
|
14341
14450
|
code: "UNREALIZED_EXPORT_HANDLE",
|
|
14342
14451
|
defaultSeverity: "warning",
|
|
14343
|
-
summary: "
|
|
14452
|
+
summary: "A declared publication handle \u2014 a method's `exportedVia`, or the router entry a listener mounts a portal through \u2014 names an export its own source file does not have: the spec names a route to the code that nobody can import"
|
|
14344
14453
|
// Deliberately NOT carryable, and so it hands over no `parts`: a wrong
|
|
14345
14454
|
// handle is one string to correct, and a parking space for it could
|
|
14346
14455
|
// only ever be a parking space for a lie.
|
|
@@ -14386,6 +14495,30 @@ var init_export_conformance = __esm({
|
|
|
14386
14495
|
draftContext
|
|
14387
14496
|
);
|
|
14388
14497
|
}
|
|
14498
|
+
for (const listener of ctx.components) {
|
|
14499
|
+
if (listener.componentType !== "Portal") continue;
|
|
14500
|
+
for (const mount of listener.mounts ?? []) {
|
|
14501
|
+
const handle = mount.via;
|
|
14502
|
+
if (!handle) continue;
|
|
14503
|
+
const portal = ctx.componentMap.get(mount.portal);
|
|
14504
|
+
if (!portal) continue;
|
|
14505
|
+
const files = realization.filesOf(portal.id).map(pathKey);
|
|
14506
|
+
const exact = files.filter((file) => {
|
|
14507
|
+
const facts = code.factsAt(file);
|
|
14508
|
+
return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
|
|
14509
|
+
});
|
|
14510
|
+
const exporters = exact.filter((file) => code.factsAt(file).exportedNames.includes(handle));
|
|
14511
|
+
for (const file of exporters) promise(file, handle);
|
|
14512
|
+
if (exporters.length > 0 || exact.length === 0 || exact.length < files.length) continue;
|
|
14513
|
+
ctx.addIssue(
|
|
14514
|
+
"warning",
|
|
14515
|
+
"UNREALIZED_EXPORT_HANDLE",
|
|
14516
|
+
`Listener "${listener.id}" mounts portal "${portal.id}" through "${handle}", but ${files.map((f) => `"${f}"`).join(", ")} \u2014 the file(s) realizing "${portal.id}" \u2014 export no such name. A mount's \`via\` names the router entry the listener calls to hand the portal its requests, published by the portal's own files, so one they do not publish promises a route nobody can import. It allows nothing either: every export of those files is still read against the contracts. Name the entry the portal's file actually exports, export it, or drop \`via\` if the listener calls the portal's contract methods directly.`,
|
|
14517
|
+
listener.id,
|
|
14518
|
+
ctx.isComponentDraft(listener.id) || ctx.isComponentDraft(portal.id)
|
|
14519
|
+
);
|
|
14520
|
+
}
|
|
14521
|
+
}
|
|
14389
14522
|
for (const type of ctx.types) {
|
|
14390
14523
|
if (!type.sourcePath) continue;
|
|
14391
14524
|
promise(type.sourcePath, type.symbol ?? type.name);
|
|
@@ -15762,6 +15895,7 @@ var init_repository = __esm({
|
|
|
15762
15895
|
init_dispatch_table_bindings();
|
|
15763
15896
|
init_dispatch_step_routing();
|
|
15764
15897
|
init_lifecycle_entrypoints();
|
|
15898
|
+
init_portal_mounts();
|
|
15765
15899
|
init_unused_detection();
|
|
15766
15900
|
init_invoked_by_description();
|
|
15767
15901
|
init_unused_types();
|
|
@@ -15903,6 +16037,9 @@ var init_repository = __esm({
|
|
|
15903
16037
|
dispatchTableBindingsRule,
|
|
15904
16038
|
dispatchStepRoutingRule,
|
|
15905
16039
|
lifecycleRule,
|
|
16040
|
+
// Which listener serves which portal: another declared edge into a
|
|
16041
|
+
// component, so its validity reads beside dispatch and lifecycle.
|
|
16042
|
+
portalMountsRule,
|
|
15906
16043
|
// The declared entrypoint's own prose before the walk that its declaration
|
|
15907
16044
|
// silences, then the walk, then the types no walk can reach.
|
|
15908
16045
|
invokedByDescriptionRule,
|
|
@@ -19620,6 +19757,10 @@ function stripNamespaceFromComponent(spec, prefix) {
|
|
|
19620
19757
|
dispatch: spec.dispatch?.map((b) => ({
|
|
19621
19758
|
...b,
|
|
19622
19759
|
component: relativizeId(b.component, prefix)
|
|
19760
|
+
})),
|
|
19761
|
+
mounts: spec.mounts?.map((m) => ({
|
|
19762
|
+
...m,
|
|
19763
|
+
portal: relativizeId(m.portal, prefix)
|
|
19623
19764
|
}))
|
|
19624
19765
|
};
|
|
19625
19766
|
}
|
|
@@ -19768,6 +19909,7 @@ function rewriteSpecRefs(raw, remap) {
|
|
|
19768
19909
|
rewriteEach(raw.dependsOn, "component");
|
|
19769
19910
|
rewriteEach(raw.owns, "component");
|
|
19770
19911
|
for (const binding of entries(raw.dispatch)) rewritePair(binding, "component", "method");
|
|
19912
|
+
for (const mount of entries(raw.mounts)) rewrite(mount, "portal", "component");
|
|
19771
19913
|
} else if (kind === "subsystem") {
|
|
19772
19914
|
for (const entrypoint of entries(raw.lifecycle)) rewritePair(entrypoint, "component", "method");
|
|
19773
19915
|
rewritePublished(raw.publicInterfaces);
|
|
@@ -19822,6 +19964,9 @@ function identityKeyOf(field, item) {
|
|
|
19822
19964
|
switch (field) {
|
|
19823
19965
|
case "dispatch":
|
|
19824
19966
|
return str(o.capability);
|
|
19967
|
+
// A listener mounts each portal once, so the portal IS the mount's identity.
|
|
19968
|
+
case "mounts":
|
|
19969
|
+
return str(o.portal);
|
|
19825
19970
|
case "lifecycle":
|
|
19826
19971
|
return `${String(o.phase)} ${String(o.component)} ${String(o.method)}`;
|
|
19827
19972
|
case "emits":
|
|
@@ -20542,6 +20687,12 @@ var init_specs2 = __esm({
|
|
|
20542
20687
|
dispatch: comp.dispatch?.map((b) => ({
|
|
20543
20688
|
...b,
|
|
20544
20689
|
component: qualifyId(b.component, namespacePrefix, this.rootSubsystems)
|
|
20690
|
+
})),
|
|
20691
|
+
// A listener's mount names a portal id, qualified like any other
|
|
20692
|
+
// component reference so a chained listener still finds its portals.
|
|
20693
|
+
mounts: comp.mounts?.map((m) => ({
|
|
20694
|
+
...m,
|
|
20695
|
+
portal: qualifyId(m.portal, namespacePrefix, this.rootSubsystems)
|
|
20545
20696
|
}))
|
|
20546
20697
|
}));
|
|
20547
20698
|
index.interfaces = index.interfaces.map((intf) => ({
|
|
@@ -22086,6 +22237,9 @@ var init_specs2 = __esm({
|
|
|
22086
22237
|
if (Array.isArray(out.dispatch)) {
|
|
22087
22238
|
out.dispatch = out.dispatch.map((b) => typeof b?.component === "string" ? { ...b, component: q2(b.component) } : b);
|
|
22088
22239
|
}
|
|
22240
|
+
if (Array.isArray(out.mounts)) {
|
|
22241
|
+
out.mounts = out.mounts.map((m) => typeof m?.portal === "string" ? { ...m, portal: q2(m.portal) } : m);
|
|
22242
|
+
}
|
|
22089
22243
|
break;
|
|
22090
22244
|
case "interface":
|
|
22091
22245
|
if (typeof out.component === "string") out.component = q2(out.component);
|
|
@@ -29923,6 +30077,11 @@ function createMcpServer(options = {}) {
|
|
|
29923
30077
|
method: import_zod11.z.string().describe("Method name on the serving component's interface"),
|
|
29924
30078
|
description: import_zod11.z.string().optional()
|
|
29925
30079
|
}).strict()).optional().describe("Portal-only: capability \u2192 component.method dispatch table for generic-handle portals. Gives the reachability walker real edges and is validated against target interfaces (UNSERVED_CAPABILITY)."),
|
|
30080
|
+
mounts: import_zod11.z.array(import_zod11.z.object({
|
|
30081
|
+
portal: import_zod11.z.string().describe("The mounted Portal's component id"),
|
|
30082
|
+
prefixes: import_zod11.z.array(import_zod11.z.string()).describe('The path prefixes this listener routes to the portal. A path lies under a prefix when it equals it or continues it past a slash, so "/" covers only the root itself'),
|
|
30083
|
+
via: import_zod11.z.string().optional().describe("The router entry the listener calls to hand the portal its request, exported by the PORTAL's own file (held to it: UNREALIZED_EXPORT_HANDLE). Omit when the listener calls the portal's contract methods directly, route by route")
|
|
30084
|
+
}).strict()).optional().describe("Portal-only: the portals this LISTENER serves, each under its path prefixes and through its router entry. Declaring the field \u2014 even as an empty array \u2014 marks this portal as a listener, the one kind of portal nothing else needs to mount. Checked by portal-mounts: a mount must name a Portal (MOUNT_TARGET_NOT_PORTAL), every HTTP endpoint of a mounted portal must lie under one of its prefixes (ENDPOINT_OUTSIDE_MOUNT), and a portal with HTTP endpoints that is neither a listener nor mounted by one is reported (UNMOUNTED_PORTAL)."),
|
|
29926
30085
|
durability: import_zod11.z.enum(["ram-projection", "durable", "read-through", "cache"]).optional().describe("Store-only \u2014 OMIT IT on any other componentType, where it is refused at the write (DURABILITY_ON_NON_STORE) and nothing is saved. Every Store should declare one (MISSING_DURABILITY): durable = persisted RAM projection (hydration read-back from a lifecycle init entrypoint required \u2014 MISSING_HYDRATION); read-through = persisted with no RAM copy (every read is the read-back, hydration exempt); ram-projection = rebuilt not restored; cache = evictable loss-safe memo state."),
|
|
29927
30086
|
dependencyClass: import_zod11.z.enum(["pure", "read"]).optional().describe("Orchestrator-only \u2014 OMIT IT on any other componentType, where it is refused at the write (DEPENDENCY_CLASS_ON_NON_ORCHESTRATOR) and nothing is saved. Declares what logic may depend on, enforced as a Store's durability is (DEPENDENCY_CLASS_VIOLATION): pure = depends only on pure Orchestrators (a computation over the values it is handed, e.g. an arbiter or codec); read = also on read Orchestrators, Repositories, Indexes and Adapters, never calling their write methods; unset = a workflow."),
|
|
29928
30087
|
emits: import_zod11.z.array(import_zod11.z.object({
|
|
@@ -29947,7 +30106,7 @@ function createMcpServer(options = {}) {
|
|
|
29947
30106
|
inputSchema: componentInput,
|
|
29948
30107
|
outputSchema: specWriteReceiptOutput
|
|
29949
30108
|
},
|
|
29950
|
-
({ id, name, description, subsystem, componentType, owns, dependsOn, portalType, basePath, dispatch, durability, dependencyClass, emits, subscribesTo, ext, status: status2 }) => {
|
|
30109
|
+
({ id, name, description, subsystem, componentType, owns, dependsOn, portalType, basePath, dispatch, mounts, durability, dependencyClass, emits, subscribesTo, ext, status: status2 }) => {
|
|
29951
30110
|
try {
|
|
29952
30111
|
const { loadSubsystemSpec: loadSubsystemSpec2, loadComponentSpec: loadComponentSpec2 } = requireSpecs();
|
|
29953
30112
|
const sub = loadSubsystemSpec2(subsystem);
|
|
@@ -29967,6 +30126,8 @@ function createMcpServer(options = {}) {
|
|
|
29967
30126
|
...portalType ? { portalType } : {},
|
|
29968
30127
|
...basePath ? { basePath } : {},
|
|
29969
30128
|
...dispatch ? { dispatch } : {},
|
|
30129
|
+
// An EMPTY list is kept: declaring the field is what marks a listener.
|
|
30130
|
+
...mounts ? { mounts } : {},
|
|
29970
30131
|
...durability ? { durability } : {},
|
|
29971
30132
|
...dependencyClass ? { dependencyClass } : {},
|
|
29972
30133
|
...emits ? { emits } : {},
|
|
@@ -30557,7 +30718,7 @@ function createMcpServer(options = {}) {
|
|
|
30557
30718
|
inputSchema: {
|
|
30558
30719
|
kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project gateway surface, each entry {id, name, subsystem, component, type, details, audience: project|department|instance|partner|external}; id is informational)"),
|
|
30559
30720
|
id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
|
|
30560
|
-
delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
|
|
30721
|
+
delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
|
|
30561
30722
|
dryRun: import_zod11.z.boolean().optional().describe("Ask what this delta WOULD do instead of doing it. The whole write runs, the candidate gate included, and the answer is the change report it would have produced \u2014 marked DRY RUN, with nothing stamped and not one byte of the stored file moved. Use it before a delta that renumbers a long narrative.")
|
|
30562
30723
|
},
|
|
30563
30724
|
outputSchema: specChangeReportOutput
|