@passioncode-ai/fabric-agent-adapter 0.72.0 → 0.73.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.
Files changed (19) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/package.json +1 -1
  3. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  4. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  5. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +2 -2
  6. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +2 -2
  7. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +1 -0
  8. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +3 -1
  9. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +3 -2
  10. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +17 -0
  11. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +841 -0
  12. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent_schema.py +496 -0
  13. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +91 -1
  14. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/service-channel-agent-extension.schema.json +700 -0
  15. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  16. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +21 -8
  17. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/practices.md +15 -6
  18. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  19. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
@@ -0,0 +1,496 @@
1
+ """The schema half of the probe's channel-agent rule (FAA-197, DEC-0150 FAC-SEM-162): the vendored
2
+ `service-channel-agent-extension.schema.json` at the contract pin, a JSON Schema reader for exactly the keywords that
3
+ file uses, giving Ajv's errors in Ajv's order (allErrors, verbose), and the contract's reading of those errors as
4
+ places (`src/channel-agent.ts`: widths, oversize's names, placedToken, allowed, placed, messagePlaces, schemaErrors).
5
+
6
+ No place carries a declared value: a member name the schema does not declare is written `*` (CA-5). Not a kit module:
7
+ the probe has no Node twin and kit.json does not list it. Standard library only, Python 3.9+."""
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import math
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any, Callable, Dict, List, Optional, Set
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+ from fabric_jsvalue import double, is_number, js_keys, js_string, json_equal # noqa: E402
19
+
20
+ SCHEMA_FILE = Path(__file__).resolve().parent / "service-channel-agent-extension.schema.json"
21
+ #: FAC-SEM-162's one answer when the schema itself cannot run on a value.
22
+ TOO_LARGE = "the value is too large to check"
23
+ #: A string value or a member name longer than this, in code points, is refused before the schema runs (CA-2).
24
+ LONGEST_STRING = 4096
25
+ #: The arrays the schema declares: a numeric token after one of them is an index a path may carry.
26
+ ARRAYS = frozenset(("projects", "codes", "tiers", "channels", "sources", "declined_sources", "handoff_queues", "recorded_decisions"))
27
+ ENUM_VALUES = 8
28
+ PATTERN_CHARACTERS = 80
29
+ _INDEX = re.compile(r"(?:0|[1-9][0-9]{0,8})\Z")
30
+ _NONE_FORM_AT = re.compile(r"(?:^|/)projects/[0-9]+/(?:question_route|gap_registry|tickets/pd_check|channels/[0-9]+/texts/final)\Z")
31
+ _TEXTS_AT = re.compile(r"(?:^|/)channels/[0-9]+/texts(?:/holding)?\Z")
32
+ _CLOSED_TEXTS_AT = re.compile(r"/channels/[0-9]+/texts\Z")
33
+
34
+ # Ajv's keyword groups in the order its generated code runs them (ajv 8, draft 2020-12): the untyped group first, then
35
+ # one group per data type, each keyword in its vocabulary's order. Only what the vendored schema uses is read.
36
+ _GROUPS = (
37
+ (None, ("$ref", "const", "enum", "not", "oneOf", "if")),
38
+ ("number", ("maximum", "minimum")),
39
+ ("string", ("maxLength", "minLength", "pattern", "format")),
40
+ ("array", ("maxItems", "minItems", "items")),
41
+ ("object", ("maxProperties", "minProperties", "required", "propertyNames", "additionalProperties", "properties")),
42
+ )
43
+ _GROUP_KEYS = {group: keys for group, keys in _GROUPS if group is not None}
44
+ #: Members a schema node may carry that change nothing: metadata, and `then`/`else`, which `if` reads.
45
+ _INERT = frozenset(("$schema", "$id", "title", "description", "$defs", "then", "else", "type"))
46
+ SUPPORTED = frozenset(k for _, keys in _GROUPS for k in keys) | _INERT
47
+ #: What Ajv counts as a rule beside a `$ref` (a `$ref` with none is read as the reference alone).
48
+ _RULES = SUPPORTED - frozenset(("$schema", "$id", "title", "description", "$defs"))
49
+ _FORMATS = ("date",)
50
+
51
+ _schema: Optional[Dict[str, Any]] = None
52
+ _names: Optional[Set[str]] = None
53
+ _patterns: Dict[str, "re.Pattern[str]"] = {}
54
+
55
+
56
+ def schema() -> Dict[str, Any]:
57
+ """The vendored schema, read once."""
58
+ global _schema
59
+ if _schema is None:
60
+ _schema = json.loads(SCHEMA_FILE.read_text(encoding="utf-8"))
61
+ return _schema
62
+
63
+
64
+ def unsupported(node: Any = None) -> List[str]:
65
+ """The keywords and formats a schema node (default: the vendored schema) uses that this reader does not read."""
66
+ found: List[str] = []
67
+ stack = [schema() if node is None else node]
68
+ while stack:
69
+ item = stack.pop()
70
+ if isinstance(item, list):
71
+ stack.extend(item)
72
+ elif isinstance(item, dict):
73
+ for key, value in item.items():
74
+ if key not in SUPPORTED:
75
+ found.append(key)
76
+ elif key == "format" and value not in _FORMATS:
77
+ found.append("format %s" % value)
78
+ if key in ("properties", "$defs"):
79
+ stack.extend(value.values())
80
+ elif key not in ("enum", "const", "required", "pattern", "description", "title"):
81
+ stack.append(value)
82
+ return sorted(set(found))
83
+
84
+
85
+ def _pattern(text: str) -> "re.Pattern[str]":
86
+ """An ECMAScript pattern (u flag) as Python reads it: `$` outside a class is the end of input only (\\Z)."""
87
+ if text not in _patterns:
88
+ out, i, in_class = [], 0, False
89
+ while i < len(text):
90
+ char = text[i]
91
+ if char == "\\":
92
+ out.append(text[i:i + 2])
93
+ i += 2
94
+ continue
95
+ if char == "[":
96
+ in_class = True
97
+ elif char == "]":
98
+ in_class = False
99
+ out.append("\\Z" if char == "$" and not in_class else char)
100
+ i += 1
101
+ _patterns[text] = re.compile("".join(out))
102
+ return _patterns[text]
103
+
104
+
105
+ def _is(data: Any, kind: str) -> bool:
106
+ if kind == "object":
107
+ return isinstance(data, dict)
108
+ if kind == "array":
109
+ return isinstance(data, list)
110
+ if kind == "string":
111
+ return isinstance(data, str)
112
+ if kind == "boolean":
113
+ return isinstance(data, bool)
114
+ if kind == "null":
115
+ return data is None
116
+ if kind in ("number", "integer"):
117
+ if not is_number(data) or not math.isfinite(double(data)):
118
+ return False
119
+ return kind == "number" or double(data) % 1 == 0
120
+ return False
121
+
122
+
123
+ _DAYS = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
124
+
125
+
126
+ def _date(text: str) -> bool:
127
+ """ajv-formats' full `date`: YYYY-MM-DD with a real month and day, leap years counted."""
128
+ match = re.match(r"([0-9]{4})-([0-9]{2})-([0-9]{2})\Z", text)
129
+ if not match:
130
+ return False
131
+ year, month, day = (int(part) for part in match.groups())
132
+ leap = year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
133
+ return 1 <= month <= 12 and 1 <= day <= (29 if month == 2 and leap else _DAYS[month])
134
+
135
+
136
+ def _pointer(key: str) -> str:
137
+ return key.replace("~", "~0").replace("/", "~1")
138
+
139
+
140
+ def _has_ref(node: Any) -> bool:
141
+ if isinstance(node, dict):
142
+ return "$ref" in node or any(_has_ref(value) for key, value in node.items() if key not in ("enum", "const"))
143
+ return isinstance(node, list) and any(_has_ref(item) for item in node)
144
+
145
+
146
+ class _Reader:
147
+ """Ajv's allErrors validation of one value, errors in its order with keyword, instancePath, schemaPath, params,
148
+ the keyword's schema (verbose) and, inside propertyNames, the member name. A definition that refers to no other is
149
+ inlined, so its errors' schemaPath starts `#/$defs/<name>`; one that does is its own function, whose paths start `#`."""
150
+
151
+ def __init__(self, root: Dict[str, Any]):
152
+ self.defs = root.get("$defs") or {}
153
+
154
+ def node(self, sch: Any, data: Any, ip: str, sp: str, errs: List[Dict[str, Any]], pn: Optional[str]) -> bool:
155
+ start = len(errs)
156
+ if sch is True:
157
+ return True
158
+ if sch is False or not isinstance(sch, dict):
159
+ errs.append(self.error("false schema", ip, sp, {}, False, pn))
160
+ return False
161
+ if "$ref" in sch and not any(key in _RULES for key in sch if key != "$ref"):
162
+ return self.ref(sch["$ref"], data, ip, errs, pn)
163
+ types = [sch["type"]] if isinstance(sch.get("type"), str) else list(sch.get("type") or [])
164
+ used = {group: any(key in sch for key in keys) for group, keys in _GROUP_KEYS.items()}
165
+ type_first = bool(types) and not (len(types) == 1 and used.get(types[0], False))
166
+ if type_first and not any(_is(data, kind) for kind in types):
167
+ errs.append(self.error("type", ip, sp + "/type", {"type": sch["type"]}, sch["type"], pn))
168
+ for group, keys in _GROUPS:
169
+ if group is not None and not used[group]:
170
+ continue
171
+ if group is not None and not _is(data, group):
172
+ if types == [group] and not type_first:
173
+ errs.append(self.error("type", ip, sp + "/type", {"type": sch["type"]}, sch["type"], pn))
174
+ continue
175
+ for key in keys:
176
+ if key in sch:
177
+ getattr(self, "k_" + key.lstrip("$"))(sch, sch[key], data, ip, sp, errs, pn)
178
+ return len(errs) == start
179
+
180
+ def error(self, keyword: str, ip: str, sp: str, params: Dict[str, Any], value: Any, pn: Optional[str]) -> Dict[str, Any]:
181
+ error = {"keyword": keyword, "instancePath": ip, "schemaPath": sp, "params": params, "schema": value}
182
+ if pn is not None:
183
+ error["propertyName"] = pn
184
+ return error
185
+
186
+ def ref(self, ref: str, data: Any, ip: str, errs: List[Dict[str, Any]], pn: Optional[str]) -> bool:
187
+ name = ref[len("#/$defs/"):]
188
+ target = self.defs[name]
189
+ return self.node(target, data, ip, "#" if _has_ref(target) else "#/$defs/" + name, errs, pn)
190
+
191
+ def k_ref(self, sch, value, data, ip, sp, errs, pn) -> None:
192
+ self.ref(value, data, ip, errs, pn)
193
+
194
+ def k_const(self, sch, value, data, ip, sp, errs, pn) -> None:
195
+ if not json_equal(data, value):
196
+ errs.append(self.error("const", ip, sp + "/const", {"allowedValue": value}, value, pn))
197
+
198
+ def k_enum(self, sch, value, data, ip, sp, errs, pn) -> None:
199
+ if not any(json_equal(data, item) for item in value):
200
+ errs.append(self.error("enum", ip, sp + "/enum", {"allowedValues": value}, value, pn))
201
+
202
+ def k_not(self, sch, value, data, ip, sp, errs, pn) -> None:
203
+ if self.node(value, data, ip, sp + "/not", [], pn):
204
+ errs.append(self.error("not", ip, sp + "/not", {}, value, pn))
205
+
206
+ def k_oneOf(self, sch, value, data, ip, sp, errs, pn) -> None:
207
+ start, passing = len(errs), []
208
+ for i, branch in enumerate(value):
209
+ if self.node(branch, data, ip, "%s/oneOf/%d" % (sp, i), errs, pn):
210
+ passing.append(i)
211
+ if len(passing) == 1:
212
+ del errs[start:]
213
+ else:
214
+ errs.append(self.error("oneOf", ip, sp + "/oneOf", {"passingSchemas": passing[:2] or None}, value, pn))
215
+
216
+ def k_if(self, sch, value, data, ip, sp, errs, pn) -> None:
217
+ branch = "then" if self.node(value, data, ip, sp + "/if", [], pn) else "else"
218
+ if branch in sch and not self.node(sch[branch], data, ip, "%s/%s" % (sp, branch), errs, pn):
219
+ errs.append(self.error("if", ip, sp + "/if", {"failingKeyword": branch}, value, pn))
220
+
221
+ def k_maximum(self, sch, value, data, ip, sp, errs, pn) -> None:
222
+ if double(data) > value:
223
+ errs.append(self.error("maximum", ip, sp + "/maximum", {"comparison": "<=", "limit": value}, value, pn))
224
+
225
+ def k_minimum(self, sch, value, data, ip, sp, errs, pn) -> None:
226
+ if double(data) < value:
227
+ errs.append(self.error("minimum", ip, sp + "/minimum", {"comparison": ">=", "limit": value}, value, pn))
228
+
229
+ def k_maxLength(self, sch, value, data, ip, sp, errs, pn) -> None:
230
+ if len(data) > value: # code points, as Ajv's ucs2length counts them
231
+ errs.append(self.error("maxLength", ip, sp + "/maxLength", {"limit": value}, value, pn))
232
+
233
+ def k_minLength(self, sch, value, data, ip, sp, errs, pn) -> None:
234
+ if len(data) < value:
235
+ errs.append(self.error("minLength", ip, sp + "/minLength", {"limit": value}, value, pn))
236
+
237
+ def k_pattern(self, sch, value, data, ip, sp, errs, pn) -> None:
238
+ if not _pattern(value).search(data):
239
+ errs.append(self.error("pattern", ip, sp + "/pattern", {"pattern": value}, value, pn))
240
+
241
+ def k_format(self, sch, value, data, ip, sp, errs, pn) -> None:
242
+ if value == "date" and not _date(data):
243
+ errs.append(self.error("format", ip, sp + "/format", {"format": value}, value, pn))
244
+
245
+ def k_maxItems(self, sch, value, data, ip, sp, errs, pn) -> None:
246
+ if len(data) > value:
247
+ errs.append(self.error("maxItems", ip, sp + "/maxItems", {"limit": value}, value, pn))
248
+
249
+ def k_minItems(self, sch, value, data, ip, sp, errs, pn) -> None:
250
+ if len(data) < value:
251
+ errs.append(self.error("minItems", ip, sp + "/minItems", {"limit": value}, value, pn))
252
+
253
+ def k_items(self, sch, value, data, ip, sp, errs, pn) -> None:
254
+ for i, item in enumerate(data):
255
+ self.node(value, item, "%s/%d" % (ip, i), sp + "/items", errs, pn)
256
+
257
+ def k_maxProperties(self, sch, value, data, ip, sp, errs, pn) -> None:
258
+ if len(data) > value:
259
+ errs.append(self.error("maxProperties", ip, sp + "/maxProperties", {"limit": value}, value, pn))
260
+
261
+ def k_minProperties(self, sch, value, data, ip, sp, errs, pn) -> None:
262
+ if len(data) < value:
263
+ errs.append(self.error("minProperties", ip, sp + "/minProperties", {"limit": value}, value, pn))
264
+
265
+ def k_required(self, sch, value, data, ip, sp, errs, pn) -> None:
266
+ for name in value:
267
+ if name not in data:
268
+ errs.append(self.error("required", ip, sp + "/required", {"missingProperty": name}, value, pn))
269
+
270
+ def k_propertyNames(self, sch, value, data, ip, sp, errs, pn) -> None:
271
+ for key in js_keys(data):
272
+ if not self.node(value, key, ip, sp + "/propertyNames", errs, key):
273
+ errs.append(self.error("propertyNames", ip, sp + "/propertyNames", {"propertyName": key}, value, pn))
274
+
275
+ def k_additionalProperties(self, sch, value, data, ip, sp, errs, pn) -> None:
276
+ declared = sch.get("properties") or {}
277
+ for key in js_keys(data):
278
+ if key in declared:
279
+ continue
280
+ if value is False:
281
+ errs.append(self.error("additionalProperties", ip, sp + "/additionalProperties", {"additionalProperty": key}, value, pn))
282
+ elif value is not True:
283
+ self.node(value, data[key], ip + "/" + _pointer(key), sp + "/additionalProperties", errs, pn)
284
+
285
+ def k_properties(self, sch, value, data, ip, sp, errs, pn) -> None:
286
+ for name, sub in value.items():
287
+ if name in data:
288
+ self.node(sub, data[name], ip + "/" + _pointer(name), "%s/properties/%s" % (sp, name), errs, pn)
289
+
290
+
291
+ def validate(value: Any) -> List[Dict[str, Any]]:
292
+ """The schema's errors on a value, as Ajv lists them; an empty list when it is valid."""
293
+ errs: List[Dict[str, Any]] = []
294
+ _Reader(schema()).node(schema(), value, "", "#", errs, None)
295
+ return errs
296
+
297
+
298
+ def schema_names() -> Set[str]:
299
+ """Every member name the schema declares: a path token that is not one (a text's language, an unknown member) is `*`."""
300
+ global _names
301
+ if _names is None:
302
+ names: Set[str] = set()
303
+ stack: List[Any] = [schema()]
304
+ while stack:
305
+ node = stack.pop()
306
+ if isinstance(node, list):
307
+ stack.extend(node)
308
+ elif isinstance(node, dict):
309
+ if isinstance(node.get("properties"), dict):
310
+ names.update(node["properties"])
311
+ stack.extend(node.values())
312
+ _names = names
313
+ return _names
314
+
315
+
316
+ def is_none(value: Any) -> bool:
317
+ """The standard's form for a required member a project does not have: an object with a `none` member (CA-2)."""
318
+ return isinstance(value, dict) and "none" in value
319
+
320
+
321
+ def placed_token(previous: Optional[str], before: Optional[str], token: str, index: bool, names: Set[str],
322
+ none_form: bool = False) -> str:
323
+ """A member's token in a FAC-SEM-162 place: a declared name, or an index of a declared array, else `*`."""
324
+ if none_form and before == "texts" and previous == "final" and token in ("none", "until"):
325
+ return token
326
+ if before == "texts" and previous in ("holding", "final"):
327
+ return "*"
328
+ if index:
329
+ return token if previous is not None and previous in ARRAYS and _INDEX.match(token) else "*"
330
+ return token if token in names else "*"
331
+
332
+
333
+ def resolved(node: Any, defs: Dict[str, Any], value: Any = None) -> Dict[str, Any]:
334
+ """A schema node with its `$ref` followed and a member that takes the none form resolved to the value's branch."""
335
+ at = node
336
+ while True:
337
+ if isinstance(at, dict) and isinstance(at.get("$ref"), str):
338
+ at = defs[at["$ref"][len("#/$defs/"):]]
339
+ elif isinstance(at, dict) and all(k in at for k in ("if", "then", "else")) and "properties" not in at:
340
+ at = at["then"] if is_none(value) else at["else"]
341
+ else:
342
+ return at if isinstance(at, dict) else {}
343
+
344
+
345
+ def widths(value: Any) -> List[str]:
346
+ """The places of every array past its maxItems, map past its maxProperties and closed object with more members than
347
+ it declares, read before the schema runs so it never makes one error per member (CA-2)."""
348
+ root = schema()
349
+ defs = root["$defs"]
350
+ found: List[str] = []
351
+ stack: List[Any] = [(value, root, "")]
352
+ while stack:
353
+ item, node, at = stack.pop()
354
+ rule = resolved(node, defs, item)
355
+ if isinstance(item, list):
356
+ if is_number(rule.get("maxItems")) and len(item) > rule["maxItems"]:
357
+ found.append("maxItems at %s (at most %s items)" % (at, js_string(rule["maxItems"])))
358
+ elif "items" in rule:
359
+ for i in range(len(item) - 1, -1, -1):
360
+ stack.append((item[i], rule["items"], "%s/%d" % (at, i)))
361
+ elif isinstance(item, dict):
362
+ count = len(item)
363
+ declared = rule.get("properties") if isinstance(rule.get("properties"), dict) else None
364
+ if declared is not None and rule.get("additionalProperties") is False and count > len(declared):
365
+ elsewhere = "; only texts.final takes the none form (Narrowings)" if _CLOSED_TEXTS_AT.search(at) and "none" in item else ""
366
+ found.append("maxProperties at %s (more members than the schema declares%s)" % (at or "/", elsewhere))
367
+ elif is_number(rule.get("maxProperties")) and count > rule["maxProperties"]:
368
+ found.append("maxProperties at %s (at most %s members)" % (at, js_string(rule["maxProperties"])))
369
+ elif declared is not None:
370
+ present = [name for name in declared if name in item]
371
+ for name in reversed(present):
372
+ stack.append((item[name], declared[name], "%s/%s" % (at, name)))
373
+ return found
374
+
375
+
376
+ def type_of(branch: Any) -> Optional[str]:
377
+ """A branch's type, through its `$ref`s into the schema's own `$defs`."""
378
+ if not isinstance(branch, dict):
379
+ return None
380
+ if isinstance(branch.get("type"), str):
381
+ return branch["type"]
382
+ if isinstance(branch.get("$ref"), str):
383
+ return type_of(schema()["$defs"].get(branch["$ref"][len("#/$defs/"):]))
384
+ return None
385
+
386
+
387
+ def name_rule(rule: Dict[str, Any]) -> str:
388
+ """What a `propertyNames` schema takes, from the schema alone: its pattern, its length, the name it refuses."""
389
+ parts = []
390
+ if isinstance(rule.get("pattern"), str):
391
+ parts.append("matching %s" % rule["pattern"])
392
+ if is_number(rule.get("maxLength")):
393
+ parts.append("at most %s code points" % js_string(rule["maxLength"]))
394
+ if isinstance(rule.get("not"), dict) and "const" in rule["not"]:
395
+ parts.append("other than %s" % js_string(rule["not"]["const"]))
396
+ return "a member name %s" % ", ".join(parts) if parts else "a member name the schema takes"
397
+
398
+
399
+ def allowed(error: Dict[str, Any], names: Set[str], at: str = "") -> str:
400
+ """What the schema allows at a failed place, from the schema alone, never the instance."""
401
+ params = error.get("params") or {}
402
+ keyword, rule = error["keyword"], error.get("schema")
403
+ texts = bool(_TEXTS_AT.search(at))
404
+ elsewhere = "; only texts.final takes the none form (Narrowings)" if texts and "none" in (params.get("additionalProperty"), params.get("propertyName")) else ""
405
+ if keyword == "required" and isinstance(params.get("missingProperty"), str) and params["missingProperty"] in names:
406
+ return " (%s)" % params["missingProperty"]
407
+ if keyword == "additionalProperties":
408
+ return " (only declared members are taken%s)" % elsewhere
409
+ if keyword == "propertyNames" and isinstance(rule, dict):
410
+ return " (%s%s)" % (name_rule(rule), elsewhere)
411
+ if keyword == "enum" and isinstance(params.get("allowedValues"), list):
412
+ values = [js_string(v) for v in params["allowedValues"]]
413
+ rest = " and %d more" % (len(values) - ENUM_VALUES) if len(values) > ENUM_VALUES else ""
414
+ return " (one of %s%s)" % (", ".join(values[:ENUM_VALUES]), rest)
415
+ if keyword == "const" and "allowedValue" in params:
416
+ return " (%s)" % js_string(params["allowedValue"])
417
+ if keyword == "pattern" and isinstance(params.get("pattern"), str):
418
+ pattern = params["pattern"]
419
+ return " (pattern %s)" % (pattern[:PATTERN_CHARACTERS] + "\u2026" if len(pattern) > PATTERN_CHARACTERS else pattern)
420
+ limit = params.get("limit") if is_number(params.get("limit")) else None
421
+ if limit is not None:
422
+ def count(word: str) -> str:
423
+ return "%s %s%s" % (js_string(limit), word, "" if limit == 1 else "s")
424
+ words = {"maxLength": "at most %s" % count("code point"), "minLength": "at least %s" % count("code point"),
425
+ "maxItems": "at most %s" % count("item"), "minItems": "at least %s" % count("item"),
426
+ "maxProperties": "at most %s" % count("member"), "minProperties": "at least %s" % count("member")}
427
+ if keyword in words:
428
+ return " (%s)" % words[keyword]
429
+ if keyword in ("minimum", "maximum"):
430
+ return " (%s %s)" % (js_string(params.get("comparison")), js_string(limit))
431
+ if keyword == "format" and isinstance(params.get("format"), str):
432
+ return " (format %s)" % params["format"]
433
+ if keyword == "not" and isinstance(rule, dict) and isinstance(rule.get("required"), list):
434
+ return " (%s only with not_yet)" % ", ".join(rule["required"])
435
+ if keyword == "not" and isinstance(rule, dict) and "const" in rule:
436
+ return " (a value other than %s)" % js_string(rule["const"])
437
+ if keyword == "type" and isinstance(params.get("type"), (str, list)):
438
+ form = ", or the none form {none: not_yet, until} / {none: not_used}" if _NONE_FORM_AT.search(at) else ""
439
+ kinds = params["type"] if isinstance(params["type"], list) else [params["type"]]
440
+ return " (%s%s)" % (" or ".join(kinds), form)
441
+ if keyword == "oneOf":
442
+ if isinstance(rule, list):
443
+ kinds = [type_of(branch) for branch in rule]
444
+ types = [kind for kind in kinds if kind is not None and kind != "null"]
445
+ if "null" in kinds and types:
446
+ return " (%s %s or null)" % ("an" if re.match(r"[aeiou]", types[0]) else "a", " or ".join(types))
447
+ return " (exactly one of its forms)"
448
+ return ""
449
+
450
+
451
+ def placed(error: Dict[str, Any], or_null: bool = False) -> str:
452
+ """An error as its keyword and its place, every free member name replaced by `*`, never a declared value."""
453
+ names = schema_names()
454
+ tokens = [t.replace("~1", "/").replace("~0", "~") for t in error["instancePath"].split("/")[1:]]
455
+ none_form = isinstance(error.get("schemaPath"), str) and not error["schemaPath"].startswith("#/$defs/texts")
456
+ at = "/".join(placed_token(tokens[i - 1] if i >= 1 else None, tokens[i - 2] if i >= 2 else None, token,
457
+ bool(_INDEX.match(token)), names, none_form) for i, token in enumerate(tokens))
458
+ return "%s at /%s%s%s" % (error["keyword"], at, allowed(error, names, at), ", or null" if or_null else "")
459
+
460
+
461
+ def message_places(places: List[str]) -> List[str]:
462
+ """The places a message names, each distinct pattern spelled once; a later one reads `(pattern as above)`."""
463
+ seen: Set[str] = set()
464
+ out = []
465
+ for written in places:
466
+ or_null = ", or null" if written.endswith(", or null") else ""
467
+ place = written[:len(written) - len(or_null)]
468
+ at = place.find(" (pattern ")
469
+ if at < 0:
470
+ out.append(written)
471
+ continue
472
+ pattern = place[at:]
473
+ if pattern in seen:
474
+ out.append("%s (pattern as above)%s" % (place[:at], or_null))
475
+ else:
476
+ seen.add(pattern)
477
+ out.append(written)
478
+ return out
479
+
480
+
481
+ def schema_errors(value: Any, validator: Callable[[Any], List[Dict[str, Any]]] = validate) -> List[str]:
482
+ """The schema's errors on a value as places: a nullable member fails in one line, its value branch's, ending
483
+ ", or null"; `if` lines and a member name's inner lines (its propertyNames line states them) are dropped."""
484
+ try:
485
+ errors = validator(value)
486
+ except RecursionError:
487
+ return [TOO_LARGE]
488
+ if not errors:
489
+ return []
490
+ nullable = {e["instancePath"] for e in errors
491
+ if e["keyword"] == "oneOf" and isinstance(e.get("schema"), list) and any(type_of(b) == "null" for b in e["schema"])}
492
+ kept = [e for e in errors if e["keyword"] != "if"
493
+ and not (e["keyword"] != "propertyNames" and e.get("propertyName") is not None)
494
+ and not (e["keyword"] == "type" and (e.get("params") or {}).get("type") == "null")
495
+ and not (e["keyword"] == "oneOf" and e["instancePath"] in nullable)]
496
+ return list(dict.fromkeys(placed(e, e["instancePath"] in nullable) for e in kept))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabric-service-kit",
3
- "version": "0.72.0",
3
+ "version": "0.73.0",
4
4
  "about": "The service kit's modules in dependency order: a module imports only modules listed before it. Written by fabric_kit.py build; edit the order and layers by hand, never the digests.",
5
5
  "python": [
6
6
  {
@@ -5766,6 +5766,96 @@
5766
5766
  "fabric-openrouter.mjs": "6041f3ab30f516658e1dbab4897a651cc8509b2136cfa8e769802c6b40ec2f35",
5767
5767
  "fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
5768
5768
  "share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
5769
+ },
5770
+ "0.73.0": {
5771
+ "fabric_jsvalue.py": "052f1b40bab18600b3731dbdca27cbff17588c21d9458fa6241efd1340d0e09a",
5772
+ "fabric_time.py": "588f383da4ac6a243ea95b6ffc3a0aa99ca94245732dc4937734f190b7af4ae9",
5773
+ "fabric_identifiers.py": "0fd814d521ac3aa39907458265870753046a73f48af3d8581ab864b93b211607",
5774
+ "fabric_secrets.py": "53c5df8d6f2c87433c288853bfaad9a43f544789bdf1e39bda8d3ff14485c89d",
5775
+ "fabric_crypto.py": "9c34aee20dbe4d6742e4fdbaab68ceb934bbe7dbd3a904c0010824ace16cdeaa",
5776
+ "fabric_base.py": "f62bcc2ed9912660fa875d27a09f60867260cc4925f79873501fd9620be59611",
5777
+ "fabric_platform.py": "a9350dff55ef0fb3a23cdb4d0320a15868b15a8deb10d2eb74d72c58740c284b",
5778
+ "fabric_paths.py": "8a1ee742f80f436e9f28c61b4f049a9545de2a9e0d66bfc61faa6ee5d1ba3734",
5779
+ "fabric_estate_rules.py": "8b36a7f4108d9c2b1fb38c8bff344778c807bed631a99b4a563ecb0a49c10b1c",
5780
+ "fabric_estate.py": "0737ae1d1bb12586bae49de1f15ef1348bc207b5007cced9dad4c55bb5870874",
5781
+ "fabric_mandate.py": "fb19ce5359606bd6e3352ab02b2a12f49605ec6e5c49b55cce1f79c831325044",
5782
+ "fabric_delegation.py": "716f0ff7ac01095f54b24970beac1962128e8ff33026b243fc0467876142ab03",
5783
+ "fabric_share.py": "da36a466426fc29dd7dd40013167f3959a18375f8fedd228bf64f298cec1040c",
5784
+ "fabric_ceremonies.py": "a59e83cf4adb2669a9b98008e3d3a0cba991a35cbff97bc604fc82fe81cfd49f",
5785
+ "fabric_package_schema.py": "e623f0408d38e3343ed66222fb812907c6c52e1a286eb1c1176ab2e64640c9ad",
5786
+ "fabric_package.py": "1f2b9e73b2b32a7e1e887d4b69f87c6e21b4d1227ad924a235334223e6dd51ca",
5787
+ "fabric_protocols.py": "077c6f03f3b887a2e22d9ce6b5bd3750199d71a29d4c82b1d5af8a615b509d93",
5788
+ "fabric_actions.py": "ae21d1199614c4f74d1dcd1e0a87107a9d52434cd9e2d64aa24a7b1e196fb723",
5789
+ "fabric_connections.py": "7de55cb7d808f756d650effabfaa12027d40a5bd5d2f6d448c32969d47529ecb",
5790
+ "fabric_a2a.py": "8b01502131215bb4de1c01866187c56e99b791543923ed8577535573db33b919",
5791
+ "fabric_page_links.py": "1b57ea573c550e2f90d1ec3edcc655917ceaa0db9dd1635fccf422ec3024d25e",
5792
+ "fabric_lock.py": "dfaf92a7086cbb097d832a305869b7964e2d2a9da0e3365c8ec1685a7bb4fef9",
5793
+ "fabric_usage.py": "5cc2e03ccc5b06e81987ed7bc3eead7a7e6cbf1a7cb8e05c6c64315eed9d91b6",
5794
+ "fabric_backups.py": "1f3f0d7ecf361b29e9101e07f1c53e5dc304d598bf26fd59ec89f16b9fed2420",
5795
+ "fabric_descriptor.py": "c08c91ca0960c694ed7677e7e9247d6307c7b63f37a7e614db022c080cd92ccc",
5796
+ "fabric_context.py": "6ae311ddc7217a4aff915e7ce2646486931acd128462b7bebe4a52bbb5fbb6c7",
5797
+ "fabric_auth.py": "74ccc10239b65a3567cec5213a7b344cbd88d6eee1fd1c5f7be1d6daf985993c",
5798
+ "fabric_events.py": "cd94b186e2a8f8cb4ad2b6bd44d83ec5021933075e28c5d655f387f16dff42b8",
5799
+ "fabric_logs.py": "86dc55bfafa2a57264722e2403510a249b95de0e4a722b2e4bc3948e3bd93da9",
5800
+ "fabric_supervisors.py": "3c5b2e8c310738720b1c1b779be4710e90251013d9be4c68f9952edc0bf55d5f",
5801
+ "fabric_service.py": "4b13f90aa26881fbfe0d9f3077fbb3552f3ce0a3c59279d6baa92df872867d8d",
5802
+ "fabric_profiles.py": "26e21ec29c016000f8ca8053b7b489184854a73be1b7fdc0a18d12990203db9b",
5803
+ "fabric_interop_rules.py": "9bdb7989e57b9d0b0d15eb9982d396499823a5e2c528230b71c67866c736e0f5",
5804
+ "fabric_interop.py": "d27e31834611a37cf2a117746f2f60fc187ccc48718eb546ff923c5d0fce7dea",
5805
+ "fabric_hub.py": "b9ac852e601b1d54cf61351677a628afc69faeed7d1fc74ebe541c1cf38e0c2a",
5806
+ "fabric_board_shapes.py": "0a0e8d3d40e33dfd8c457aedb85330b2ff25f8c49a33eb6cbd5a5cc8a5007f7e",
5807
+ "fabric_board_link.py": "052bc3c474ece835bb428c37ca864810d04e394d991ddb9c93989a7a825073a5",
5808
+ "fabric_board_slots.py": "92b065e3df935547cf46653245f1129e4a1f8b59b0a41e8fa9ffdc41596ee592",
5809
+ "fabric_board_session.py": "1fd3c3884f3044b56a40f78b33ff88dcac9272284f6681ea359c662879826f7e",
5810
+ "fabric_board_fixture.py": "69761ce205ec640762ec9b591c6903e5a87f844e8d423b041681924027e39d1e",
5811
+ "fabric_board.py": "f86c40b8291f8c818f6924910f4022b013d5fd107abaf8825dd0aa6c5e418638",
5812
+ "fabric_http.py": "8b443477dc29d5a1fe908b9dccaf68f05fab837eae0670074051a1dd2dfb71ad",
5813
+ "fabric_openrouter.py": "d06a40f83f2fb8bd441ab3485ae882f1fbdd9cd7a9c1ec175eb47cb8ae2cce81",
5814
+ "fabric-jsvalue.mjs": "dfbdb8c41584972c9389750a41719374a1cfc5652ed438dd1623025190b0106e",
5815
+ "fabric-time.mjs": "3ff932069038a720df1c68c6492976906737b2e67e5c12d9c81c418597f661de",
5816
+ "fabric-identifiers.mjs": "a18c2a501a4aa170bfd3d99e571ad6b7662649b4d3b9d2a540c002ea42811bbf",
5817
+ "fabric-secrets.mjs": "5822a7e25ecf2e87b5bab4f99c1074af7379d8b49e2784283b5d7d8c25022cde",
5818
+ "fabric-crypto.mjs": "038e03689701e3c9721a1e7f8e0a4d02a52d6a5d1661b0dc84035edc3ebbfcf6",
5819
+ "fabric-base.mjs": "7804b2df7715dbbf9702a9b61b6b8cef2110f9a52b04cabd8fb0a66458d87512",
5820
+ "fabric-platform.mjs": "a8119d95ebba4d589a9e401f5d6fc8dda62d59d44e4c1ff811d97fac3771ac0c",
5821
+ "fabric-paths.mjs": "aa630d611e748a899e7043480f4a4e46b444f2e15ff945f30c2fbef21c1ceea5",
5822
+ "fabric-estate-rules.mjs": "7476be71174aa5b1026134f2b335abf647feaa55584e2f9de8618e3054aa8e1e",
5823
+ "fabric-estate.mjs": "e8a2e62d2313121e6cb502f6e6abad94d1d546a44b4d9b3ce2be29e7808e8cf3",
5824
+ "fabric-mandate.mjs": "b70f3de03a659e60bba7b5a0580b3582825cd7b8a8e30a28c79621919ec5b7bb",
5825
+ "fabric-delegation.mjs": "8f63a63aab9a8746b7f664720b87e9398137d01b8d50880974d5070038e414a0",
5826
+ "fabric-share.mjs": "bfb26581c21ad984c2a337da02429f9f3a165cc582f8ba48de4d3dfb7d163a59",
5827
+ "fabric-ceremonies.mjs": "117d4374c9e95c930a38de7adbd4323774798f4fe6ec6f24afcd6f2c7298ca44",
5828
+ "fabric-package-schema.mjs": "cd28092375db0fc21ffab1f0bb7794f7f8979ee08c64d3b9faf4b3845357caa6",
5829
+ "fabric-package.mjs": "8cfb32eebbd4e1bcf28a6e4437344ae2dd54479b0e7c820f002e9f94ab9ecaad",
5830
+ "fabric-protocols.mjs": "a560b7d6eb0d26204c5536711c3dda9daae90cc91e07cdc75412e75c86272767",
5831
+ "fabric-actions.mjs": "06e42f5e24c96311d5417d0d1effa93479ef7128a7ab139903b2bcf59253f6be",
5832
+ "fabric-connections.mjs": "303432e547ce563e90ca9ffcd827f5612f51d3582929834e1add0fdaf6485dcc",
5833
+ "fabric-a2a.mjs": "51b72fe1d80649d659fcad3a8f96370db3476c4a7b30cbabd8522c138be69346",
5834
+ "fabric-page-links.mjs": "a8834926e9d4bbf7c4857f551451d2ebf6ac8873bbccaaf89c5e30cf45b17ee6",
5835
+ "fabric-lock.mjs": "02cf833c97a07a8b3c788f9c93f70c4b9c9066efa25318dccf0038e0fc79717f",
5836
+ "fabric-usage.mjs": "1c4d18004639409624ac3516a02f2b60bbf55abdb612e9a96c37e83666eb87a7",
5837
+ "fabric-backups.mjs": "cfd8c9085e3d597a783099c2bd2ffe2f3faa0daaadab46dedd30523894cfeb06",
5838
+ "fabric-descriptor.mjs": "7c0ef7d245c037eb9e859ab481f6e66fb3e5ef2cf6d1e0a8d32b6d303097ca43",
5839
+ "fabric-context.mjs": "1112d695e872f9825cbc94f08484974bff3880087bc29b8be59f885f03e8f7be",
5840
+ "fabric-auth.mjs": "10ff37a39f9e3adb4d0f05db66c45f49b10eef582e9220c213907964109d7e52",
5841
+ "fabric-events.mjs": "c463d6ffcae24229a5c608d8fcea4268d2ccc5024b19fa10e5a452284068fa71",
5842
+ "fabric-logs.mjs": "58467e5719c16e099d05bbedec47a95712506ab03c33f3cd4be1252e8498a946",
5843
+ "fabric-supervisors.mjs": "d45c38de0cb3ed85e083496c2d3c51dab004454accdd87f4762a512a6d442573",
5844
+ "fabric-service.mjs": "297fef11ac76dc831d12d24c5581b7a93587c300c20664db703b3f924c0f9f1a",
5845
+ "fabric-profiles.mjs": "314376cf2deb87f235fb18b11d3b532e211f585b9af0a9302b5f4cc39971b532",
5846
+ "fabric-interop-rules.mjs": "b4e386c359a48e49ffff8a842215760c18a7c98a0af4754a808a35fb2820d458",
5847
+ "fabric-interop.mjs": "989040aed33b2712a258244192bdf70e811a51076aa1b62f2adc2a6db051dbed",
5848
+ "fabric-hub.mjs": "c005115a864cfd67daa4862097bb14acfe5a60bf653897e5bf8a570dd80f1070",
5849
+ "fabric-board-shapes.mjs": "c1db7b7bd0d58d82bb6d8dfbc3cd1547a256a333052d367e82ff1b3edf2804d7",
5850
+ "fabric-board-link.mjs": "6e7d51f4fd9730d93455f50dd30e97907b8b9e1ca65a1d69f036f2e8ec05cc0d",
5851
+ "fabric-board-slots.mjs": "fc77a3c6e62aa18575568bef4f703cce2edaba8cef648813ebcdbb21bdc27a11",
5852
+ "fabric-board-session.mjs": "18555962e5f6d18080a3e7971c4c6640452542f6042d07917e7ebc073721c415",
5853
+ "fabric-board-fixture.mjs": "83c4b7e33ee47b68385faf2569d38d5c8a0c5cb0f8656090f41e1b0eed2f436e",
5854
+ "fabric-board.mjs": "15fbb4441fcf7d2f285375b75de64ad6cffea129c515084c110245376b07775f",
5855
+ "fabric-http.mjs": "317d7668c291570d4d93bc73295312f2853eb93940f4cf754a4a4619cb87ace9",
5856
+ "fabric-openrouter.mjs": "6041f3ab30f516658e1dbab4897a651cc8509b2136cfa8e769802c6b40ec2f35",
5857
+ "fabric-package.schema.json": "2ba799f576fad0f608029223c4ccccd122152f1c5dbfa8ce8b31cd63ac92ad44",
5858
+ "share-registration.schema.json": "34b3997eeaf936ebc861342a23045634a7c18c19f01d7ed4198310d81989f5b0"
5769
5859
  }
5770
5860
  },
5771
5861
  "data": {