Skip to content

Release 1.2.3a1#74

Open
github-actions[bot] wants to merge 128 commits into
masterfrom
release-1.2.3a1
Open

Release 1.2.3a1#74
github-actions[bot] wants to merge 128 commits into
masterfrom
release-1.2.3a1

Conversation

@github-actions

Copy link
Copy Markdown

Human review requested!

JarbasAl and others added 30 commits May 22, 2026 11:59
Adds the remaining two intent primitives.

- resources.py — `LocaleResources`, the OVOS-INTENT-2 loader: the §3
  common reader (UTF-8/BOM/CRLF, comments, blanks), recursive
  `locale/<lang>/` search with case-insensitive tags, the user→skill→core
  override precedence (§2.1), and the five resource roles (§4). The
  user-data path is a parameter — no configuration import. Duplicate
  resources and empty files raise `MalformedResource`.
- dialog.py — `render()`, the OVOS-INTENT-2 §4.2 dialog renderer: select
  a phrase, expand its variety, fill `{name}` slots from caller values.
  Expansion precedes fill so a slot value is never parsed as grammar.
  An unfilled slot raises `UnfilledSlot`.

20 more conformance tests (58 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The package is the reference implementation of the OVOS formal
specifications generally, not only the intent specs — the
formal-specifications repo will grow beyond OVOS-INTENT-1/2/3. Renamed
from ovos-intent-primitives: repo, distribution, and the import package
(ovos_intent_primitives -> ovos_spec_tools).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A CLI and library function that validates every resource file under a
locale directory and reports every problem found.

- syntax (OVOS-INTENT-1): each template expanded; malformed forms,
  empty files, and named slots in slot-free roles are errors
- naming/layout (OVOS-INTENT-2): base-name charset, .entity slot-name
  rule, duplicate (role, base name), language-tag form, files outside a
  language directory, legacy file types
- `ovos-spec-lint <locale>` — accepts a locale/ or a <lang>/ directory;
  exit code non-zero on errors, or on warnings with --strict (CI-ready)

15 linter tests (73 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
examples/dirty-locale is a deliberately broken skill locale — every
file trips at least one ovos-spec-lint check (two valid files produce
no findings). examples/README.md shows the locale, the command, the
verbatim linter output (10 errors, 4 warnings), and a table mapping
each finding to its spec rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
An OO alternative to render(). Beyond holding the phrases, vocabularies
and rng, it tracks the last phrase chosen and avoids repeating it on the
next call — repetition avoidance, which a stateless function cannot do
and which OVOS-INTENT-2 §4.2 explicitly allows.

- DialogRenderer(phrases, vocabularies=, rng=) with .render(slots=)
- DialogRenderer.from_resources(resources, name) builds one from a
  LocaleResources
- render() refactored to share _render_phrase with the class

6 more tests (79 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…a Protocol

- rng parameters are now typed `Chooser`, a Protocol (an object with a
  `choice` method) instead of the meaningless `object`. `random` and a
  `random.Random` instance both satisfy it.
- DialogRenderer holds default slot values (set once, reused every
  render) and `.entity` value sets. A slot resolves in order: per-call
  value, default, a random `.entity` value, then UnfilledSlot.
- DialogRenderer.from_resources now also loads `.entity` value sets and
  accepts default `slots`.
- LocaleResources.entities() — every `.entity` for the language, as a
  name -> expanded value set map (mirrors vocabularies()).

6 more tests (85 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One self-contained .py per feature worth demonstrating:

- expand.py — the sentence template expander
- load_resources.py — the locale resource loader
- render_dialog.py — render() and DialogRenderer (repetition
  avoidance, default slots, .entity fallback)
- lint.py — the linter used as a library

Adds examples/skill-locale/, a small valid skill locale the loader and
dialog scripts read. examples/README.md indexes the scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the requested language has no directory, LocaleResources resolves
to the nearest available language (OVOS-INTENT-2 §2.2, non-normative) —
e.g. en-AU finds en-US.

- nearness is decided by a LanguageMatcher Protocol (an object with
  tag_distance(desired, supported)); the langcodes module satisfies it
  structurally and is the default
- langcodes moved from a hard dependency to the [langcodes] extra; the
  package core now has zero dependencies. Without langcodes, resolution
  is exact-match only
- max_language_distance caps the fallback (default 10, per §2.2);
  set 0 to disable

5 more tests (90 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Language is dynamic — a locale folder is a skill's multilingual unit —
so it must not be pinned at construction.

- LocaleResources: drops the `lang` constructor argument; every load
  method takes `lang`. One instance serves every language, and the
  smart fallback is resolved afresh per call.
- DialogRenderer: now resource-backed and multilingual — built from a
  LocaleResources + a dialog name, with `render(lang, slots=)`.
  Repetition avoidance is tracked per language. The `render()` function
  stays as the stateless, language-agnostic primitive.
- New ovos_spec_tools/language.py — `standardize_lang` and
  `closest_lang`, the single home for the language-tag logic OVOS
  reimplements across locale loading, TTS voices and STT models
  (ovos_utils.lang.get_language_dir, phoonnx.match_lang, ...). Mirrors
  their proven behaviour: tag standardization, distance-below-10 match,
  the Tagalog `tl`/`fil` quirk. langcodes stays optional.
- LocaleResources uses closest_lang; a custom `lang_resolver` may be
  injected. The LanguageMatcher protocol is replaced by this.

9 more tests (99 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s one

langcodes resolves a bare tag to its most-populous region, so "pt"
matches "pt-BR" closer than "pt-PT". The unmarked form of a language
should resolve to its reference variety: Portuguese is "from Portugal",
and every Lusophone country except Brazil follows the pt-PT norm.

closest_lang() now, for a bare language tag with a norm region
(_NORM_REGION, seeded with pt -> PT), prefers a candidate in that
region over the langcodes distance result. So a request for "pt"
against a locale offering pt-PT and pt-BR resolves to pt-PT. An
explicit pt-BR request is still respected; a bare tag still falls back
by distance when the norm region is absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bsent

Without langcodes no tag distance can be computed, so closest_lang
previously resolved exact matches only. It now adds a final fallback: a
candidate sharing the primary subtag — so a request for en-AU still
accepts en, en-GB, en-US, ... It prefers the bare language tag, then the
norm region, then the first match.

This fallback also covers the rare case where langcodes is installed but
computes no in-threshold distance.

3 more tests (105 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous closest_lang was a cascade of special cases — exact, then
norm region, then langcodes distance, then a primary-subtag fallback.
That logic now lives in a single distance function.

- lang_distance(a, b) — the one place policy lives: standardizes tags,
  measures a bare tag from its norm region (so pt -> pt-PT is 0, not a
  langcodes population guess), uses langcodes when present, and falls
  back to a coarse same-language measure (shared primary subtag is near,
  the generic form nearer than a sibling region) when it is not.
- closest_lang(target, available, max_distance) — now branch-free: the
  candidate with the smallest lang_distance, accepted if below the cap.
- lang_distance is exported as a public primitive.

108 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New docs/ — an ordered guide that builds from a first install to the
full API:

- getting-started — install and a taste of every tool
- templates — the OVOS-INTENT-1 grammar
- locale-resources — the locale/ folder and the five file roles
- dialog — render() and DialogRenderer
- language-matching — standardize_lang, lang_distance, closest_lang
- linting — ovos-spec-lint, on the CLI and in CI
- api-reference — every public name

The README drops its long per-tool sections in favour of a quick taste
and links into docs/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
34 new tests (142 total):

- expansion: whitespace/tab normalization, optional vocabularies arg,
  bare <name> templates, multi-word vocabulary members, empty
  vocabulary, no cross-template slot-consistency check, optional around
  a slot, unicode literals, duplicate-branch de-duplication
- resources: core fallback and skill-overrides-core precedence, empty
  .dialog/.voc, vocabularies()/entities() directly, indented and
  mid-line "#", nonexistent skill_locale, undefined <voc> reference
- dialog: single-phrase renderer, no-slot phrases, a slot value
  containing braces stays literal, numeric values, missing dialog
- language: empty available list, empty-string tag, regional vs
  different-language distance, regioned-request sibling fallback
- lint: nonexistent path, empty locale, unknown extension ignored,
  a single-language directory argument

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New checks:
- §5.5 slot consistency: every template in one .intent/.dialog must
  declare the same slot set — flagged when they differ (previously
  unchecked)
- a non-UTF-8 file is now reported as an error instead of crashing the
  linter (UnicodeDecodeError is not an OSError)
- a .blacklist with no matching .intent — a warning (orphan suppression)
- a language directory with no resource files — a warning

New --spec-version {0,1,2} flag — a forward-compatibility check that
flags features newer than a target runtime:
- spec-version 0: a .blacklist file warns (a v0 runtime ignores it)
- spec-version < 2: a <name> reference errors (an older runtime cannot
  expand the template)
- default 2: nothing extra flagged

11 more linter tests (153 total). The dirty-locale example output is
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cifications)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of a documentation and interoperability effort for OpenVoiceOS,
funded by NLnet under grant agreement 101135429.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
AI-Generated Change:
- Model: claude-sonnet-4-6
- Intent: add missing CI/CD workflows
- Impact: added build-tests.yml, coverage.yml, lint.yml, license_check.yml, pip_audit.yml, release_workflow.yml, publish_stable.yml, release-preview.yml, repo-health.yml, conventional-label.yml
- Verified via: reviewed apply_workflows.py output
Implements the `.prompt` resource role: a localized, whole-file
plain-text prompt for a language model.

- prompt.py — render_prompt() and PromptRenderer. {name} substitution is
  conservative: only a well-formed name, only names the caller supplied,
  never inside a ``` fenced code block. Unfilled slots and any other
  braces are left literal.
- resources.py — read_prompt_file() (whole/verbatim read) and
  LocaleResources.load_prompt(); PROMPT_ROLE constant.
- 22 tests (175 total); docs and an examples/render_prompt.py added.

The linter's .prompt awareness (and --spec-version 3) remains a
follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The linter now recognizes `.prompt` as a resource role instead of
silently ignoring it as an unknown extension.

- `.prompt` is a known role: collected, deduplicated, and naming-checked
  like the others; no longer mistaken for a legacy/unknown file.
- A `.prompt` is checked as a whole-file document, not a template — only
  non-emptiness and a valid UTF-8 read; never template syntax, slot-free
  or §5.5 checks.
- A non-UTF-8 `.prompt` is reported, not crashed.
- --spec-version gains 3 (the .prompt role); default is now 3. Below it,
  a `.prompt` file is a warning — an older runtime ignores the role.

6 tests (181 total); docs/linting.md updated. dirty-locale output
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g policy (#4)

* feat: lang_matches and iter_locale_dirs — pin the cross-component lang policy

Every place that crossed a component boundary (resource loaders,
adapt/padatious/padacioso engine bucketing, TTS/STT plugin routing,
`_get_closest_lang` boilerplate) reimplemented the same three steps
— standardize, distance, threshold-check — with subtle drift between
the copies. Bug surface: a skill stores resources under a full tag
but an engine buckets under a macroed one (or vice versa), and the
two never reconcile.

This adds the two missing primitives so callers stop reinventing them:

* `lang_matches(a, b, max_distance=10) -> bool` — the
  `if lang_distance(...) < threshold` check, named and shared. Pass
  `max_distance=0` for exact-match.

* `iter_locale_dirs(root, native_langs=None) -> Iterator[(lang, path)]`
  — walks `<root>/locale/` and yields each subdir as a canonical
  `(standardize_lang(name), Path)` pair, optionally filtered against
  the skill's native langs via `closest_lang`. Resource loaders for
  `.rx`, `.dialog`, `.voc`, `.intent`, locale `.json` reinvent this
  walk by hand — switch them to this and the macro/full-tag
  disagreement disappears.

The implicit policy: **full normalized lang tag everywhere**.
Macro-stripping is no longer a knob; if an engine buckets under
`en` and a resource is `en-US`, the engine reconciles at match time
via `closest_lang`, not at registration time via macro flags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: remove unused MalformedTemplate import, rename ambiguous loop var, sync docstring

- Drop MalformedTemplate from resources.py import (F401 — unused)
- Rename loop variable l -> lang in iter_locale_dirs comprehension (E741)
- Add lang_matches and iter_locale_dirs to the top-level module docstring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: pass install_extras as pip args and expose license classifier

- install_extras was 'test' (bare) → reusable workflow ran 'pip install test',
  no such PyPI package. Pass '.[test]' instead.
- pip-licenses categorised the package itself as 'Error' (unknown license)
  because pyproject had no License classifier. Add the SPDX Apache-2.0
  classifier and matching Python-version classifiers.
- Tighten requires-python to >=3.10 to match the tested matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: exclude ovos-spec-tools from auditing itself

pilosus/action-pip-license-checker queries PyPI metadata for each
installed package. The PyPI release predates the SPDX License
classifier added in this PR, so the package is audited as 'Error'
(unknown license) against itself. Exclude it from the audit until the
next alpha republishes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: build-tests install_extras takes the bare extras name

The build-tests reusable workflow wraps it as `${WHEEL}[${EXTRAS}]`,
so it expects `test` not `.[test]`. The coverage workflow expects raw
pip args, so that one keeps `.[test]`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: warn_only license_check until alpha republishes

The pilosus 'exclude' regex did not suppress self-audit; switch to
warn_only so the job no longer blocks. Transitive dependencies still
get audited and reported in the PR comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng) (#6)

Every callsite outside this module that needed to locate a resource
file ended up reinventing the override-precedence + closest-lang walk
the loader already encapsulates. Promote the previously private
_locate method to a public find — same body, fuller docstring,
covered by six new tests.

The internal load_intent / load_dialog / load_prompt callers move to
the new name. The duplicate-resource check is preserved, so a
LocaleResources.find caller that violates the §2 uniqueness rule gets
the same MalformedResource it would have got via load_*.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_samples (#8)

* feat: keyword_form / vocabulary_keywords / utterance_contains / strip_samples

Every consumer that registered slot-free keywords or matched a vocab
against an utterance reimplemented the same handful of operations.
Promote them to ovos-spec-tools so the OVOS-INTENT-2 §4.3 keyword
convention has exactly one implementation.

New free functions in ovos_spec_tools.resources:

- keyword_form(template_line, vocabularies=None) -> (entity, aliases)
  expands one slot-free template, lowercases, dedupes and sorts; the
  first item is the canonical entity, the rest are aliases that
  canonicalize to it. Malformed input yields ('', []) rather than
  poisoning a batch.

- normalize_for_match(text, ensure_ascii=True) -> str
  lowercases, strips, and optionally folds accents and ASCII
  punctuation; the comparison normalization shared by the two below.

- utterance_contains(utterance, samples, exact=False, ensure_ascii=True)
  true iff utterance matches any sample — whole-word substring by
  default, exact when requested, accent/punct-folded by default.

- strip_samples(utterance, samples) -> str
  remove every whole-word occurrence of any sample, longest first so
  composite phrases consume their parts before fallback matches.

New methods on LocaleResources:

- vocabulary_keywords(lang) -> Iterator[(voc_name, entity, aliases)]
- entity_keywords(lang)    -> Iterator[(entity_name, entity, aliases)]
  yield one triple per template line, with the entity/alias split
  applied via keyword_form. Suit any consumer that registers keyword
  sets with a primary/alias distinction.

Tests: 52 passed (16 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: granular normalization flags + permissive whole-word match

- normalize_for_match now takes two keyword-only flags:
  strip_diacritics (NFD + drop combining marks) and strip_punct (drop
  ASCII punctuation, keep slot braces). The previous ensure_ascii knob
  conflated the two — French ou/où or technical names like c++ need
  separate control. Both default True; either flag can be flipped
  independently.

- utterance_contains and strip_samples forward both flags and the
  whole-word match anchor moves from \b...\b to (?<!\w)...(?!\w),
  which preserves the \b semantics on word-char boundaries while also
  matching samples that begin or end in non-word characters (c++,
  yes!). Without this, a sample ending in punctuation could never
  match in non-stripped mode.

Coverage on the new helpers expanded from 16 to 41 tests covering:
each flag independently and in combination, exact-mode normalization,
slot marker preservation, regex metachar escaping, unicode samples,
malformed lines, blank-sample filtering, override precedence walking,
voc-reference resolution at expansion time, and the missing-language
branch in _keywords_for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JarbasAl and others added 30 commits June 27, 2026 18:58
)

The INTENT-3 §4.2 well-formedness MUSTs were enforced only in lint_locale,
not in the IntentBuilder/Intent data model, so an invalid intent could be
built and registered on the bus. Add Intent.validate() enforcing both §4.2
MUSTs and call it from the construction path (IntentBuilder.build) and the
emission path (Intent.to_keyword_payload):

- (a) a keyword intent MUST declare at least one required or one-of
  constraint — an optional/excluded-only intent has nothing that must be
  present and is malformed;
- (b) a vocabulary MUST appear under at most one role — the same vocab under
  two roles (e.g. required and excluded) is contradictory and malformed.

Raises the new MalformedIntent. Raw Intent(...) construction stays permissive
(the scaffold / wire round-trip path via open_intent_envelope), so validation
fires only at build/emit and a subclass overriding build() is unaffected.

INTENT-1 §5.5 (DialogRenderer rejecting non-identical slot sets) is already
enforced on dev via verify_slot_consistency in both render paths.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…3 §4.2) (#59)

IntentBuilder.build() and Intent.to_keyword_payload() validated the intent
and RAISED MalformedIntent. Because ovos-workshop re-exports this
IntentBuilder, build()-raises broke skills that build a constraint-less
intent — a backward-compat regression on consumers' dev (e.g. workshop's
test_build_preserves_name). Build/emit now LOG a warning instead; the
raising enforcement stays available via the explicit Intent.validate()
method and the locale linter, so §4.2 is still enforced where rejection is
wanted, without breaking construction.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Both classes defined __eq__ but no __hash__, so Python set __hash__ to
None and they were unhashable -- forcing downstream lru_cache callers
(padacioso/linha-fina/nebulento) to pass frozenset workarounds instead
of the objects themselves.

Add __hash__ to both, derived from the SAME fields each __eq__ compares
so equal objects always hash equal (the hash/eq contract):
- Session.__hash__ from to_dict()
- Message.__hash__ from (msg_type, data, context)

A shared _freeze() helper (in message.py, imported by session.py)
recursively converts nested dict/list/set payloads into a deterministic
hashable form. It preserves value-equality: {"x": 1} and {"x": 1.0}
freeze equal (unlike a json.dumps digest, which would diverge "1" vs
"1.0" and violate the contract). The hash is a point-in-time snapshot of
these mutable objects -- safe for lru_cache keys and short-lived dict/set
membership, documented in the method docstrings.

Not a spec change: no message/session semantics or spec docs touched.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
PIPELINE-1 §8 makes the handler-lifecycle trio orchestrator-owned: the
orchestrator (ovos-core) emits ovos.intent.handler.{start,complete,error}
authoritatively, while the skill framework keeps emitting the legacy
mycroft.skill.handler.* as a private done-signal. Bridging the two would
(a) double-emit the spec trio (core's emission + the mirrored legacy) and
(b) reshape a shape-changing event with a lossy best-effort transform.

Remove the 3 trio entries from MIGRATION_MAP, their MIGRATION_PAYLOAD_TRANSFORMS
pair, and the now-unused _handler_legacy_to_spec/_handler_spec_to_legacy
helpers. The INTENT_HANDLER_* SpecMessage members stay (still the spec topics
core emits); only the legacy<->spec bridge for them is removed. Tests assert
the trio no longer migrates.
Make SpecMessage the spec-complete and spec-only vocabulary of static
ovos.* bus topics. Adds the topics defined by specs not previously
catalogued and traces every member to its owning spec clause.

Added (34 static members):
- SESSION-2 §2.7: SESSION_SYNC (ovos.session.sync)
- CONVERSE-1 §6.1: CONVERSE_ACTIVE_LIST(+_RESPONSE)
- PERSONA-1 §11: PERSONA_{QUERY,ANSWER,LIST,LIST_RESPONSE,REGISTER,
  DEREGISTER,ACTIVATED,DISMISSED}
- FALLBACK-1 §3: FALLBACK_{REGISTER,DEREGISTER}
- COMMON-QUERY-1 §6: COMMON_QUERY_{PING,PONG}
- TRANSFORM-1 §6: TRANSFORMER_{AUDIO,UTTERANCE,METADATA,INTENT,DIALOG,
  TTS}_LIST(+_RESPONSE) — six static query/response pairs
- OCP-1 §4: COMMON_PLAY_{PLAY,SEARCH,PAUSE,RESUME,STOP,NEXT,PREVIOUS,
  SEEK,PLAYER_STATE,MEDIA_STATE,TRACK_STATE}

Fixed: UTTERANCE_CANCELLED is owned by TRANSFORM-1 §8.2 ("defined
here"), not PIPELINE-1 §6.4 (which only references it).

Deliberately OUT of the enum:
- ovos.session.update_default / ovos.session.start: used by
  ovos-bus-client but no spec defines them (SESSION-2 §1 defers
  lifecycle topics); legitimately bare strings in the bus client.
- ovos.context.set/.unset/.clear: only a stray TRANSFORM-1 reference
  mis-cites CONTEXT-1 §5; CONTEXT-1 §5.3 routes context mutations
  through ovos.session.sync, so no ovos.context.* topic is spec-defined.
- runtime-templated topics (MSG-1 §2.1.1): <skill_id>.converse.{ping,
  pong}, <skill_id>:{converse,response}, <skill_id>.fallback.{ping,
  pong}, <skill_id>:fallback, <skill_id>:common_query, <pipeline_id>...

No members removed (every pre-existing member is spec-defined).
MIGRATION_MAP unchanged: the new topics' legacy counterparts are not
clean payload-compatible 1:1 renames, so they are flagged rather than
mapped (consistent with the #63 handler-trio decision).

Tests: TestSpecCompleteness asserts the enum equals the golden static
spec-topic set exactly (complete + spec-only), that bus-client
internals stay out, and that no templated topic is a member.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ing (#67)

* feat: SessionManager singleton registry + forward/reply session stamping

Add a SessionManager (implementation detail, not a spec) that keeps one live
Session per id and folds incoming snapshots onto it (SESSION-1 §2/§4
value-passing). Message.forward/reply now re-stamp the derived message with the
live session for its id, so a snapshot deep-copied before a handler mutated the
session cannot desync the wire. Documented with the why-it-works argument (a
stamp is always either a meaningful refresh or a no-op, never a discard) and a
canonical get->mutate->forward example.

- Session.update_from: spec-compliant fold via serialize/deserialize round-trip
  (present-empty overrides, null->default), resolved through type(self) so a
  subclass rebuilds as itself.
- SessionManager.session_cls is pluggable so a downstream layer (ovos-bus-client)
  builds its richer Session subclass; construction via deserialize stays agnostic
  of subclass __init__ signatures.
- Stamping is transparent to pure spec usage: a session-less message or an id the
  registry never folded is carried verbatim (§5), so MSG-1 forward/reply
  semantics are preserved (all 513 tests green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: idless/absent session normalizes to default on stamp (§4.3)

A Message with no session, or a session naming no session_id, IS the default
session per OVOS-SESSION-1 §4.1/§4.3 — stamp it. Only a NAMED id the registry
never folded is carried verbatim (a relay's remote session we must not
overwrite). Update the MSG-1 deep-copy test to a named (verbatim) session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ror) (#69)

get_default_session now keys off the shared sessions[DEFAULT_SESSION_ID] rather
than a per-class default_session mirror. When a subclass (ovos-bus-client) and
this base both reach the one registry, a class-attribute mirror would shadow per
class and diverge — the shared dict is a single object, so both see the same
default. default_session is kept as a re-synced convenience mirror.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ion) (#71)

The default session is a normal session per SESSION-1 §4 — the wire is
value-passing, so a message carrying the default id folds onto the live default
like any other id. Drop the owner-only guard in SessionManager.get that refused
to fold default-id snapshots (legacy baggage that violated the spec). A session
naming no id is normalized to the default id and folded.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…mp) (#73)

When the caller supplies a session via the context= kwarg of reply/response,
that is a deliberate author choice — honour it and skip the live-session
refresh, so a hand-constructed session is never overwritten by the registry. A
context= that overrides only routing keys still gets the session refreshed.
forward has no context kwarg, so it always stamps (unchanged).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant