pyflightstream 0.2.0__py3-none-any.whl

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 (71) hide show
  1. pyflightstream/__init__.py +27 -0
  2. pyflightstream/cases/__init__.py +332 -0
  3. pyflightstream/cases/matrix_legacy.py +337 -0
  4. pyflightstream/commands/__init__.py +544 -0
  5. pyflightstream/commands/_meta.yaml +18 -0
  6. pyflightstream/commands/actuators.yaml +167 -0
  7. pyflightstream/commands/advanced_settings.yaml +136 -0
  8. pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
  9. pyflightstream/commands/boundary_conditions.yaml +153 -0
  10. pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
  11. pyflightstream/commands/coordinate_systems.yaml +123 -0
  12. pyflightstream/commands/file_io.yaml +82 -0
  13. pyflightstream/commands/mesh_import_export.yaml +72 -0
  14. pyflightstream/commands/motion_definitions.yaml +185 -0
  15. pyflightstream/commands/probe_points.yaml +116 -0
  16. pyflightstream/commands/runtime_settings.yaml +160 -0
  17. pyflightstream/commands/scenes.yaml +14 -0
  18. pyflightstream/commands/script_controls.yaml +39 -0
  19. pyflightstream/commands/simulation_controls.yaml +30 -0
  20. pyflightstream/commands/solver_analysis.yaml +118 -0
  21. pyflightstream/commands/solver_export.yaml +138 -0
  22. pyflightstream/commands/solver_initialization.yaml +95 -0
  23. pyflightstream/commands/solver_settings.yaml +120 -0
  24. pyflightstream/commands/streamlines.yaml +63 -0
  25. pyflightstream/commands/surface_sections.yaml +145 -0
  26. pyflightstream/commands/sweeper.yaml +115 -0
  27. pyflightstream/commands/unsteady_solver.yaml +68 -0
  28. pyflightstream/commands/volume_sections.yaml +148 -0
  29. pyflightstream/farfield/__init__.py +613 -0
  30. pyflightstream/files/__init__.py +375 -0
  31. pyflightstream/fsi/__init__.py +57 -0
  32. pyflightstream/fsi/beam.py +417 -0
  33. pyflightstream/fsi/centrifugal.py +418 -0
  34. pyflightstream/fsi/cli.py +206 -0
  35. pyflightstream/fsi/config.py +316 -0
  36. pyflightstream/fsi/driver.py +501 -0
  37. pyflightstream/fsi/kinematics.py +153 -0
  38. pyflightstream/fsi/loads.py +740 -0
  39. pyflightstream/fsi/nodes.py +463 -0
  40. pyflightstream/fsi/state.py +181 -0
  41. pyflightstream/post/__init__.py +18 -0
  42. pyflightstream/post/writers.py +195 -0
  43. pyflightstream/probes/__init__.py +453 -0
  44. pyflightstream/probes/geometry.py +281 -0
  45. pyflightstream/probes/planar.py +477 -0
  46. pyflightstream/qa/__init__.py +59 -0
  47. pyflightstream/qa/cli.py +364 -0
  48. pyflightstream/qa/compat.py +285 -0
  49. pyflightstream/qa/drift.py +406 -0
  50. pyflightstream/qa/geometry.py +421 -0
  51. pyflightstream/qa/physics.py +1744 -0
  52. pyflightstream/qa/probes.py +1023 -0
  53. pyflightstream/qa/references/PHY-01.yaml +38 -0
  54. pyflightstream/qa/references/PHY-02.yaml +28 -0
  55. pyflightstream/qa/references/PHY-05.yaml +29 -0
  56. pyflightstream/qa/references/PHY-06.yaml +90 -0
  57. pyflightstream/qa/references/SMI-01.yaml +28 -0
  58. pyflightstream/qa/references/SMI-02.yaml +28 -0
  59. pyflightstream/qa/specs.py +1405 -0
  60. pyflightstream/reference.py +465 -0
  61. pyflightstream/results/__init__.py +547 -0
  62. pyflightstream/run/__init__.py +677 -0
  63. pyflightstream/script/__init__.py +474 -0
  64. pyflightstream/script/helpers.py +844 -0
  65. pyflightstream/versions.py +191 -0
  66. pyflightstream-0.2.0.dist-info/METADATA +88 -0
  67. pyflightstream-0.2.0.dist-info/RECORD +71 -0
  68. pyflightstream-0.2.0.dist-info/WHEEL +5 -0
  69. pyflightstream-0.2.0.dist-info/entry_points.txt +3 -0
  70. pyflightstream-0.2.0.dist-info/licenses/LICENSE +21 -0
  71. pyflightstream-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,544 @@
1
+ """The FlightStream command database and per-version registry.
2
+
3
+ Pipeline role: single source of truth for which ASCII commands exist in
4
+ which FlightStream version, with typed arguments, script layout, emission
5
+ phase, and a manual page citation (``manual_ref``) per entry. The script
6
+ builder validates every emission against this database.
7
+
8
+ Data lives in the YAML files next to this module, one file per manual
9
+ chapter; ``_meta.yaml`` holds the ordered version list, which is the only
10
+ ordering authority (CLAUDE.md invariant 4). Version keys in the YAML
11
+ files are quoted strings ("26.120"); an unquoted key would be parsed as
12
+ a float and rejected by the loader.
13
+
14
+ Statuses follow the evidence rules of CLAUDE.md invariant 3:
15
+ ``documented`` cites the manual through ``manual_ref``; ``verified`` and
16
+ ``broken`` additionally cite a committed probe report; ``removed``
17
+ records the manual page stating the removal and, when known, a
18
+ successor command.
19
+
20
+ A command whose argument grammar differs between versions declares the
21
+ grammar of the latest documented version in ``args`` and overrides it
22
+ per version through ``versions.<v>.args``; the per-version view
23
+ resolves the override, so the script builder binds and renders the
24
+ grammar of its target version (the four-versus-three argument forms of
25
+ CREATE_BULK_SEPARATION, SRC-003 p.342 versus SRC-725 p.341, are the
26
+ motivating case).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import enum
32
+ import re
33
+ from collections.abc import Iterator, Mapping
34
+ from dataclasses import dataclass
35
+ from functools import lru_cache
36
+ from importlib import resources
37
+
38
+ import yaml
39
+ from pydantic import BaseModel, ConfigDict, field_validator, model_validator
40
+
41
+ from pyflightstream.versions import FsVersion, known_versions, resolve
42
+
43
+ _MANUAL_REF_PATTERN = re.compile(r"^SRC-\d{3} pp?\.\d+")
44
+
45
+
46
+ class Layout(enum.StrEnum):
47
+ """Script layout grammars a command can use.
48
+
49
+ ``bare`` has no arguments; ``inline`` takes arguments on the command
50
+ line; ``param_lines`` takes the command name alone and its
51
+ parameters each on a following line, ended by a blank line (the
52
+ multi-line function grammar of SRC-003 p.279, for example OPEN on
53
+ p.282); ``payload_lines`` takes a count followed by that many data
54
+ lines; ``keyword_block`` takes KEY VALUE lines until a terminator.
55
+ """
56
+
57
+ BARE = "bare"
58
+ INLINE = "inline"
59
+ PARAM_LINES = "param_lines"
60
+ PAYLOAD_LINES = "payload_lines"
61
+ KEYWORD_BLOCK = "keyword_block"
62
+
63
+
64
+ class Phase(enum.StrEnum):
65
+ """Script phases, in emission order.
66
+
67
+ The script builder tracks the highest phase reached and rejects a
68
+ command whose phase precedes it (SAD phase-ordering rule).
69
+ ``control`` marks script-control commands (STOP, PRINT, log export;
70
+ SRC-003 pp.281-283) that may appear anywhere; the builder exempts
71
+ them from phase ordering.
72
+ """
73
+
74
+ GEOMETRY = "geometry"
75
+ SETUP = "setup"
76
+ INIT = "init"
77
+ EXEC = "exec"
78
+ ANALYSIS = "analysis"
79
+ EXPORT = "export"
80
+ CONTROL = "control"
81
+
82
+
83
+ class Status(enum.StrEnum):
84
+ """Evidence status of a command in one FlightStream version.
85
+
86
+ ``documented``: the manual says so (manual_ref is the evidence).
87
+ ``verified``: a Tier 2 probe passed on a licensed machine.
88
+ ``broken``: a probe recorded a manual-versus-reality discrepancy.
89
+ ``removed``: the manual states the command is no longer supported.
90
+ """
91
+
92
+ DOCUMENTED = "documented"
93
+ VERIFIED = "verified"
94
+ BROKEN = "broken"
95
+ REMOVED = "removed"
96
+
97
+
98
+ class ArgType(enum.StrEnum):
99
+ """Argument types a command can declare.
100
+
101
+ ``int_list`` is a comma-separated list of integer indices on one
102
+ data line, the grammar FlightStream uses for boundary and surface
103
+ selections (for example SRC-003 p.319). ``float_list`` is a list of
104
+ real values, for example the custom sweep values of
105
+ SWEEPER_SET_AOA_SWEEP (SRC-003 p.406). ``str_list`` is a list of
106
+ preformatted composite tokens, one per data line, used where a
107
+ single line pairs an index with a toggle, such as the per-surface
108
+ ``index,ENABLE`` lines of INITIALIZE_SOLVER (SRC-003 p.337); the
109
+ typed pair validation lives in the curated helper that emits it.
110
+ ``bool`` is a presence keyword of a keyword_block: True emits the
111
+ bare keyword line and False (or omission) emits nothing, the
112
+ grammar of the CLEAR keyword of IMPORT (SRC-003 p.307).
113
+ """
114
+
115
+ INT = "int"
116
+ FLOAT = "float"
117
+ STR = "str"
118
+ BOOL = "bool"
119
+ PATH = "path"
120
+ INT_LIST = "int_list"
121
+ FLOAT_LIST = "float_list"
122
+ STR_LIST = "str_list"
123
+ ENUM = "enum"
124
+ ENUM_LIST = "enum_list"
125
+
126
+
127
+ class ListSeparator(enum.StrEnum):
128
+ """How a list-typed argument joins its values in the script.
129
+
130
+ The manual samples show three grammars: comma-separated on one
131
+ line (SRC-003 p.332), space-separated on one line (p.364), and one
132
+ value per line (pp.338, 352).
133
+ """
134
+
135
+ COMMA = "comma"
136
+ SPACE = "space"
137
+ NEWLINE = "newline"
138
+
139
+
140
+ class CommandNotInVersionError(LookupError):
141
+ """A command is unavailable in the requested FlightStream version.
142
+
143
+ Raised by a per-version view when the command is removed in that
144
+ version or has no recorded evidence for it. The message carries the
145
+ manual citation and the successor command when one is recorded.
146
+ """
147
+
148
+
149
+ class ArgSpec(BaseModel):
150
+ """Typed specification of one command argument.
151
+
152
+ Attributes
153
+ ----------
154
+ name : str
155
+ Argument name, English, lowercase.
156
+ type : ArgType
157
+ Value type; ``enum`` and ``enum_list`` restrict values to
158
+ ``values``.
159
+ values : tuple of str, optional
160
+ Allowed tokens; required for ``enum`` and ``enum_list`` types
161
+ and forbidden otherwise.
162
+ unit : str, optional
163
+ Physical unit of the value as the solver expects it (for
164
+ example ``"m/s"``); absent for dimensionless or textual
165
+ arguments.
166
+ required : bool
167
+ Whether the argument must be supplied; optional arguments are
168
+ the ones the manual marks as such (for example
169
+ LOAD_SOLVER_INITIALIZATION of OPEN, SRC-003 p.282).
170
+ separator : ListSeparator
171
+ How a list-typed argument joins its values when rendered; the
172
+ manual fixes it per command (see :class:`ListSeparator`).
173
+ own_line : bool
174
+ For inline commands whose file path follows on the line after
175
+ the inline arguments (for example SET_PROP_ACTUATOR_PROFILE,
176
+ SRC-003 pp.323-324).
177
+ joins_previous : bool
178
+ The value is appended to the script line of the preceding
179
+ argument instead of taking its own KEY VALUE line; the copy
180
+ count that PERIODIC symmetry appends to the SYMMETRY line of
181
+ INITIALIZE_SOLVER (SRC-003 p.337) is the documented case.
182
+ """
183
+
184
+ model_config = ConfigDict(extra="forbid", frozen=True)
185
+
186
+ name: str
187
+ type: ArgType
188
+ values: tuple[str, ...] | None = None
189
+ unit: str | None = None
190
+ required: bool = True
191
+ separator: ListSeparator = ListSeparator.COMMA
192
+ own_line: bool = False
193
+ joins_previous: bool = False
194
+
195
+ @model_validator(mode="after")
196
+ def _enum_types_carry_values(self) -> ArgSpec:
197
+ is_enum = self.type in (ArgType.ENUM, ArgType.ENUM_LIST)
198
+ if is_enum and not self.values:
199
+ raise ValueError(f"argument {self.name!r} is {self.type} and must list its values")
200
+ if not is_enum and self.values is not None:
201
+ raise ValueError(f"argument {self.name!r} is {self.type} and must not list values")
202
+ return self
203
+
204
+ @property
205
+ def is_list(self) -> bool:
206
+ """Whether the argument holds several values."""
207
+ return self.type in (
208
+ ArgType.INT_LIST,
209
+ ArgType.FLOAT_LIST,
210
+ ArgType.STR_LIST,
211
+ ArgType.ENUM_LIST,
212
+ )
213
+
214
+ @model_validator(mode="after")
215
+ def _separator_only_for_lists(self) -> ArgSpec:
216
+ if self.separator is not ListSeparator.COMMA and not self.is_list:
217
+ raise ValueError(f"argument {self.name!r} is scalar and takes no separator")
218
+ return self
219
+
220
+ @model_validator(mode="after")
221
+ def _joins_previous_only_for_scalars(self) -> ArgSpec:
222
+ if self.joins_previous and self.is_list:
223
+ raise ValueError(f"argument {self.name!r} is a list and cannot join the previous line")
224
+ return self
225
+
226
+
227
+ def _check_layout_rules(name: str, layout: Layout, args: tuple[ArgSpec, ...]) -> None:
228
+ """Reject an argument tuple that contradicts its command's layout.
229
+
230
+ Shared between the entry-level ``args`` and every per-version
231
+ override, so an override cannot smuggle in a grammar the layout
232
+ renderer does not support.
233
+ """
234
+ if layout is Layout.BARE and args:
235
+ raise ValueError(f"{name} has layout bare and must not declare arguments")
236
+ if layout is not Layout.INLINE and any(arg.own_line for arg in args):
237
+ raise ValueError(f"{name}: own_line only applies to inline commands")
238
+ if layout is not Layout.KEYWORD_BLOCK and any(arg.type is ArgType.BOOL for arg in args):
239
+ raise ValueError(
240
+ f"{name}: bool arguments are bare presence keywords of a "
241
+ "keyword_block (SRC-003 p.307); other layouts spell their toggles "
242
+ "as ENABLE/DISABLE enums"
243
+ )
244
+ for position, arg in enumerate(args):
245
+ if not arg.joins_previous:
246
+ continue
247
+ if layout is not Layout.KEYWORD_BLOCK or position == 0:
248
+ raise ValueError(
249
+ f"{name}: joins_previous requires a keyword_block layout and a "
250
+ "preceding argument line to append to"
251
+ )
252
+
253
+
254
+ class VersionStatus(BaseModel):
255
+ """Evidence record of a command in one FlightStream version.
256
+
257
+ Attributes
258
+ ----------
259
+ status : Status
260
+ Evidence status; see :class:`Status`.
261
+ successor : str, optional
262
+ Replacement command name; only meaningful for ``removed``.
263
+ note : str, optional
264
+ Short paraphrased justification, with citation when needed.
265
+ report : str, optional
266
+ Repository-relative path of the committed probe report; required
267
+ for ``verified`` and ``broken`` (CLAUDE.md invariant 3).
268
+ args : tuple of ArgSpec, optional
269
+ Per-version argument grammar override. Declared when this
270
+ version's manual documents a different signature than the
271
+ entry-level ``args``; the per-version view substitutes it, so
272
+ emission for this version binds and renders the overridden
273
+ grammar. Absent for a version whose grammar matches the
274
+ entry-level one, and meaningless for ``removed``.
275
+ """
276
+
277
+ model_config = ConfigDict(extra="forbid", frozen=True)
278
+
279
+ status: Status
280
+ successor: str | None = None
281
+ note: str | None = None
282
+ report: str | None = None
283
+ args: tuple[ArgSpec, ...] | None = None
284
+
285
+ @model_validator(mode="after")
286
+ def _statuses_follow_the_evidence_rules(self) -> VersionStatus:
287
+ if self.status in (Status.VERIFIED, Status.BROKEN) and not self.report:
288
+ raise ValueError(
289
+ f"status {self.status} requires a committed probe report; statuses are "
290
+ "promoted only through pyfs-qa apply-compat, never edited by hand"
291
+ )
292
+ if self.successor is not None and self.status is not Status.REMOVED:
293
+ raise ValueError("successor is only recorded for removed commands")
294
+ if self.args is not None and self.status is Status.REMOVED:
295
+ raise ValueError(
296
+ "a removed version has no grammar to emit; args overrides are only "
297
+ "recorded for versions where the command exists"
298
+ )
299
+ return self
300
+
301
+
302
+ class CommandEntry(BaseModel):
303
+ """One command of the FlightStream scripting interface.
304
+
305
+ Attributes
306
+ ----------
307
+ name : str
308
+ Command name as the solver script expects it; supplied by the
309
+ loader from the YAML mapping key.
310
+ chapter : str
311
+ Stem of the chapter YAML file the entry came from; supplied by
312
+ the loader and used to group the generated reference.
313
+ layout : Layout
314
+ Script layout grammar.
315
+ phase : Phase
316
+ Emission phase used by the script builder's ordering check.
317
+ args : tuple of ArgSpec
318
+ Typed argument specifications, in emission order.
319
+ manual_ref : str
320
+ Manual citation, for example ``"SRC-003 p.352"``. Paraphrase
321
+ evidence only; manual text is never reproduced.
322
+ versions : mapping of str to VersionStatus
323
+ Evidence per canonical version identifier (quoted ``"26.XXX"``
324
+ keys). Versions without an entry have no recorded evidence.
325
+ notes : str, optional
326
+ Paraphrased usage caveats with citations.
327
+ """
328
+
329
+ model_config = ConfigDict(extra="forbid", frozen=True)
330
+
331
+ name: str
332
+ chapter: str = ""
333
+ layout: Layout
334
+ phase: Phase
335
+ args: tuple[ArgSpec, ...] = ()
336
+ manual_ref: str
337
+ versions: dict[str, VersionStatus]
338
+ notes: str | None = None
339
+
340
+ @model_validator(mode="before")
341
+ @classmethod
342
+ def _version_keys_are_quoted_strings(cls, data: dict) -> dict:
343
+ versions = data.get("versions")
344
+ if isinstance(versions, dict):
345
+ for key in versions:
346
+ if not isinstance(key, str):
347
+ raise ValueError(
348
+ f"version key {key!r} parsed as {type(key).__name__}; quote canonical "
349
+ 'identifiers in the YAML ("26.120")'
350
+ )
351
+ return data
352
+
353
+ @field_validator("manual_ref")
354
+ @classmethod
355
+ def _manual_ref_cites_a_page(cls, value: str) -> str:
356
+ if not _MANUAL_REF_PATTERN.match(value):
357
+ raise ValueError(
358
+ f"manual_ref {value!r} must cite a source and page, for example 'SRC-003 p.352'"
359
+ )
360
+ return value
361
+
362
+ @model_validator(mode="after")
363
+ def _versions_are_registered_and_present(self) -> CommandEntry:
364
+ if not self.versions:
365
+ raise ValueError(f"{self.name} records no version evidence")
366
+ registered = {version.canonical for version in known_versions()}
367
+ unknown = set(self.versions) - registered
368
+ if unknown:
369
+ raise ValueError(
370
+ f"{self.name} references unregistered versions {sorted(unknown)}; register "
371
+ "them in commands/_meta.yaml first"
372
+ )
373
+ return self
374
+
375
+ @model_validator(mode="after")
376
+ def _args_obey_the_layout_rules(self) -> CommandEntry:
377
+ _check_layout_rules(self.name, self.layout, self.args)
378
+ return self
379
+
380
+ @model_validator(mode="after")
381
+ def _version_arg_overrides_obey_the_layout_rules(self) -> CommandEntry:
382
+ for canonical, record in self.versions.items():
383
+ if record.args is not None:
384
+ label = f"{self.name} ({canonical} args override)"
385
+ _check_layout_rules(label, self.layout, record.args)
386
+ return self
387
+
388
+ def status_in(self, version: FsVersion) -> VersionStatus | None:
389
+ """Return the evidence record for ``version``, honoring hotfix inheritance.
390
+
391
+ A hotfix build (last canonical digit not zero) inherits the
392
+ record of its base release until probe evidence overrides it
393
+ (SAD Section 2).
394
+
395
+ Parameters
396
+ ----------
397
+ version : FsVersion
398
+ Registered version to look up.
399
+
400
+ Returns
401
+ -------
402
+ VersionStatus or None
403
+ The evidence record, or None when the command has no
404
+ recorded evidence for this version.
405
+ """
406
+ record = self.versions.get(version.canonical)
407
+ if record is not None:
408
+ return record
409
+ base_canonical = version.canonical[:-1] + "0"
410
+ if base_canonical != version.canonical:
411
+ return self.versions.get(base_canonical)
412
+ return None
413
+
414
+
415
+ @dataclass(frozen=True)
416
+ class VersionView:
417
+ """Read-only view of the commands available in one FlightStream version.
418
+
419
+ Obtained through :meth:`CommandRegistry.for_version`. Mapping-style
420
+ access raises :class:`CommandNotInVersionError` with the manual
421
+ citation when a command is removed or has no recorded evidence.
422
+ """
423
+
424
+ version: FsVersion
425
+ _registry: CommandRegistry
426
+
427
+ def __getitem__(self, name: str) -> CommandEntry:
428
+ """Return the entry for ``name`` or explain why it is unavailable."""
429
+ entry = self._registry.commands.get(name)
430
+ if entry is None:
431
+ raise CommandNotInVersionError(
432
+ f"{name} is not in the command database. The database only holds "
433
+ "commands drafted from the manual with citations; see CONTRIBUTING "
434
+ "for how to add one."
435
+ )
436
+ record = entry.status_in(self.version)
437
+ if record is None:
438
+ recorded = ", ".join(
439
+ f"{canonical} ({entry.versions[canonical].status})"
440
+ for canonical in sorted(entry.versions)
441
+ )
442
+ raise CommandNotInVersionError(
443
+ f"{name} has no recorded evidence for FlightStream {self.version.canonical}. "
444
+ f"Recorded evidence: {recorded}. Earlier versions await release-notes review "
445
+ "or backfill probing."
446
+ )
447
+ if record.status is Status.REMOVED:
448
+ reason = record.note or "no longer supported"
449
+ successor = (
450
+ f"Use {record.successor} instead."
451
+ if record.successor
452
+ else "No direct successor is recorded."
453
+ )
454
+ last = self._last_documented(entry)
455
+ last_part = f" Last documented in {last.canonical}." if last else ""
456
+ raise CommandNotInVersionError(
457
+ f"{name} is removed in FlightStream {self.version.canonical} "
458
+ f"({reason}, {entry.manual_ref}).{last_part} {successor}"
459
+ )
460
+ if record.args is not None:
461
+ # Per-version grammar override: the returned entry carries the
462
+ # argument signature this version's manual documents.
463
+ return entry.model_copy(update={"args": record.args})
464
+ return entry
465
+
466
+ def __contains__(self, name: str) -> bool:
467
+ """Return whether ``name`` is available in this version."""
468
+ try:
469
+ self[name]
470
+ except CommandNotInVersionError:
471
+ return False
472
+ return True
473
+
474
+ def __iter__(self) -> Iterator[str]:
475
+ """Iterate over the command names available in this version."""
476
+ return (name for name in self._registry.commands if name in self)
477
+
478
+ def _last_documented(self, entry: CommandEntry) -> FsVersion | None:
479
+ documented = [
480
+ version
481
+ for version in known_versions()
482
+ if entry.versions.get(version.canonical) is not None
483
+ and entry.versions[version.canonical].status is not Status.REMOVED
484
+ ]
485
+ return documented[-1] if documented else None
486
+
487
+
488
+ @dataclass(frozen=True)
489
+ class CommandRegistry:
490
+ """The whole command database, all versions.
491
+
492
+ Attributes
493
+ ----------
494
+ commands : mapping of str to CommandEntry
495
+ Every command, keyed by name, loaded from the chapter YAML
496
+ files next to this module.
497
+ """
498
+
499
+ commands: Mapping[str, CommandEntry]
500
+
501
+ @classmethod
502
+ @lru_cache(maxsize=1)
503
+ def load(cls) -> CommandRegistry:
504
+ """Load and validate the whole database from the installed package.
505
+
506
+ Returns
507
+ -------
508
+ CommandRegistry
509
+ Validated registry; every entry satisfied the schema and
510
+ the evidence rules.
511
+
512
+ Raises
513
+ ------
514
+ ValueError
515
+ If two chapter files define the same command name, or an
516
+ entry violates the schema (pydantic validation error).
517
+ """
518
+ commands: dict[str, CommandEntry] = {}
519
+ package = resources.files("pyflightstream.commands")
520
+ for resource in sorted(package.iterdir(), key=lambda item: item.name):
521
+ if not resource.name.endswith(".yaml") or resource.name == "_meta.yaml":
522
+ continue
523
+ entries = yaml.safe_load(resource.read_text(encoding="utf-8")) or {}
524
+ for name, body in entries.items():
525
+ if name in commands:
526
+ raise ValueError(f"{name} is defined in more than one chapter file")
527
+ chapter = resource.name.removesuffix(".yaml")
528
+ commands[name] = CommandEntry(name=name, chapter=chapter, **body)
529
+ return cls(commands=commands)
530
+
531
+ def for_version(self, version: str | FsVersion) -> VersionView:
532
+ """Return the view of the commands available in one version.
533
+
534
+ Parameters
535
+ ----------
536
+ version : str or FsVersion
537
+ Canonical identifier, display alias, or resolved version.
538
+
539
+ Returns
540
+ -------
541
+ VersionView
542
+ Per-version, read-only view.
543
+ """
544
+ return VersionView(version=resolve(version), _registry=self)
@@ -0,0 +1,18 @@
1
+ # Ordered FlightStream version list: release order, append only.
2
+ # This list is the only ordering authority (CLAUDE.md invariant 4).
3
+ # canonical: 26.XXX scheme, last digit indexes vendor hotfix builds.
4
+ # alias: the vendor-facing release name.
5
+ versions:
6
+ - canonical: "26.000"
7
+ alias: "26.0"
8
+ - canonical: "26.100"
9
+ alias: "26.1"
10
+ - canonical: "26.120"
11
+ alias: "26.12"
12
+
13
+ manual_editions:
14
+ "26.100": >
15
+ SRC-725: Altair FlightStream User Manual, 26.1 edition (local,
16
+ _private/exe/FlightStream_26100/), scripting reference from p.278;
17
+ cited in version notes as "SRC-725 p.N"
18
+ "26.120": "SRC-003: FlightStream User Manual v2612, scripting reference pp. 286-371"