Skip to content

Release 0.18.0a1#62

Open
github-actions[bot] wants to merge 110 commits into
masterfrom
release-0.18.0a1
Open

Release 0.18.0a1#62
github-actions[bot] wants to merge 110 commits into
masterfrom
release-0.18.0a1

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 03:18
…n) (#41)

* fix: locale/template/lint/language spec-conformance (audit remediation)

Remediate the spec-conformance flags from the adversarial audit, each tied
to the cited INTENT-1/2/3 clause:

- expansion.py inline_keywords: drop the silent max_values=10 truncation
  (data loss). Default now inlines ALL values; an explicit max_values bound
  REFUSES (raises MalformedTemplate) when exceeded per INTENT-1 §4.3
  (refuse-and-document, never silently drop).
- expansion.py inline_keywords: replace the `for _ in range(8)` depth cap
  with proper recursion + cycle detection (INTENT-1 §4.1) — unbounded
  resolution that raises on a reference cycle, no magic depth limit.
- dialog.py: implement the INTENT-1 §7 / §5.5 MUST — verify_slot_consistency
  checks all phrases of a dialog declare the same slot set; both render
  paths call it before rendering and raise on divergence.
- lint.py: a divergent .intent slot set is now an ERROR (was WARNING),
  matching .dialog. INTENT-1 §5.5, INTENT-2 §4.1/§4.2, INTENT-3 §5.1 all
  mandate rejection; removed the misread INTENT-3 §5.3 (a worked example
  that itself violates §5.5) justification.
- language.py: relabel the pt-PT norm-region and tl/tgl Tagalog overrides as
  NON-NORMATIVE implementation policy (they deliberately diverge from
  langcodes, which §2.2 endorses); annotate _coarse_distance 3/5/100 as
  arbitrary ordering-only values with no spec basis (§2.2 silent on the
  no-langcodes case).
- resources.py keyword_form: comment clarifying its deliberate leniency is
  confined to the best-effort keyword extractors; the conformant loaders
  (_load_expanded etc.) call expand() directly and raise.

Docs (linting.md, dialog.md traceability, spec-traceability.md) updated to
match. Tests updated for the new ERROR severity, no-truncation, cycle
rejection, and §5.5 dialog checks.

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

* fix: .intent MAY declare different slot sets (union); slot-consistency ERROR is .dialog-only

Corrects the audit-remediation overreach: INTENT-2 §4.1 / INTENT-3 §5.1 (per #56/#67) allow .intent templates to declare different slot sets (union); only .dialog (§4.2) requires identical slots.

* feat: validate required_slots are declared by a template (OVOS-INTENT-3 §5.3)

A required slot MUST be declared by at least one template in the intent;
declaring a required slot no template mentions is malformed and a tool MUST
reject the definition at registration time (OVOS-INTENT-3 §5.3).

required_slots is an intent-definition field above the raw .intent file, so
the locale linter (which sees only files) cannot enforce it. Expose the check
as public functions for the registration/loading path that has both in hand:

- declared_slots(templates): the union of named slots across templates,
  folding {{name}} to {name} (§3.4).
- validate_required_slots(required_slots, templates): raises MalformedTemplate
  if any required slot is declared by no template.
- lint_required_slots(path, required_slots, templates): a Finding-returning
  wrapper for linting callers.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…istered field) (#45)

SESSION-1 §3 registers fallback_handlers (array of string, owner
OVOS-FALLBACK-1 §4). It was falling through to opaque extras instead of
being a first-class registered field.

Add it to _LIST_OVERRIDE_FIELDS (same array-of-string, other-spec bucket
as the blacklists / transformer chains): constructor param, _as_str_list
attribute, generic to_dict / from_dict round-trip with empty-list-as-
omission (§3.4 / §2.1), and SESSION1_REGISTERED_FIELDS membership via the
_LIST_OVERRIDE_FIELDS union. Like converse_handlers, OVOS-FALLBACK-1 is a
forward reference (unmerged); the field is carried on the strength of the
SESSION-1 §3 registration.

Refs SESSION-1 §3, OVOS-FALLBACK-1 §4.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ord model, adapt-free) (#46)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ad translation (#42)

* fix: message-domain conformance + NamespaceTranslator per-topic payload translation

Part 1 — citation/docstring conformance fixes:
- messages.py: AUDIO_OUTPUT_STARTED/ENDED + MIC_LISTEN were mis-cited to
  AUDIO-IN-1; they are owned by OVOS-AUDIO-1 (audio-out.md, open PR #38,
  unmerged) §5.1/§5.2/§4.4 -> re-cited and marked genuinely PROVISIONAL.
- messages.py: LISTENER_RECORD_STARTED/ENDED, LISTENER_SLEEP, LISTENER_AWOKEN
  are AUDIO-IN-1 §6.1-§6.4 (MERGED) -> mandated, stale 'provisional' removed.
- messages.py: relabel NamespaceTranslator/new_mirror_guard dual-emit +
  mirror-window dedup as non-normative IMPLEMENTATION POLICY (MSG-1 §5.4
  disavows host-side correlation).
- message.py: serialize() now refuses an empty 'type' (MSG-1 §2.1/§7 gate).
- message.py: deserialize() rejects present-but-non-object data/context
  ([], 0, false, ...) instead of silently coercing to {} (MSG-1 §6/§7).

Part 2 — per-topic payload translation (maintainer-directed feature):
- Add MIGRATION_PAYLOAD_TRANSFORMS (legacy topic -> (legacy_to_spec,
  spec_to_legacy)) with transforms for the 5 shape-changing renames
  (handler trio, detach_intent, enable/disable). Payload-compatible renames
  default to identity.
- Add NamespaceTranslator.translate_payload(from_topic, to_topic, data) that
  selects direction and applies the transform (identity if none). Bus wiring
  (ovos-bus-client / ovos-utils FakeBus) is a follow-up.
- Update MIGRATION_MAP / module docstrings: drop the false 'legacy consumers
  keep working without code changes' blanket claim; document lossy cases.

Tests: +25 (serialize empty-type, deserialize wrong-type, transform
round-trips, identity, direction selection). 410 pass.

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

* fix: AUDIO-1 merged — drop provisional labels + add §7 bus-surface topics

AUDIO-1 (audio-out.md) is merged, so its mandated topics are no longer
provisional. Strip the unmerged/PR#38/PROVISIONAL qualifiers from the audio
output signals (ovos.audio.output.{started,ended} §5.1/§5.2, ovos.mic.listen
§4.4) across the module docstring, the SpecMessage block, and the
MIGRATION_MAP comments.

Add the 6 AUDIO-1 §7 bus-surface topics the enum was missing (enum-only,
not legacy renames): SPEAK_B64 (§3.4), AUDIO_SPEECH (§4.3), AUDIO_QUEUE
(§4.1), AUDIO_PLAY_SOUND (§4.2), AUDIO_STOP (§6), AUDIO_IS_SPEAKING (§5.3).

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

* docs: make message-domain docstrings timeless and standalone

Drop development-process framing (merged/now-mandated/no-longer/historically)
from the message.py and messages.py docstrings; state the current spec-defined
behaviour directly. Runtime-state phrasing (a 'previously disabled' intent, a
'previously-seen' mirror Message) is unchanged.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: make intent + bus-namespace docs timeless and standalone

Restate the intent primitives and the namespace-migration docs in terms of
current behaviour rather than development history: drop 'historically provided
by ovos-workshop', 'reimplementation/replacement', 'OVOS is moving ... off the
historical names', and 'still landing'. Describes what the code and the
migration map ARE, readable without knowing the project's history.

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

* docs: state AUDIO-IN-1/AUDIO-1 bus topics as defined, not provisional

The listener-lifecycle (AUDIO-IN-1 §6) and mic/audio-output (AUDIO-1 §4.4/§5)
specs are merged; split the conflated table row by owning spec and drop the
'provisional / prose not yet finalized' caveat.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The six OVOS-AUDIO-1 §7 output topics are present in the SpecMessage enum but
were enum-only — they had no MIGRATION_MAP entry, so the bus could not bridge
the legacy Mycroft-era handler names during the migration window.

Add the legacy->spec renames (legacy names verified against ovos-audio
register_handlers); all are payload-compatible 1:1 renames, so they bridge
with the identity payload transform (no MIGRATION_PAYLOAD_TRANSFORMS entry):

- speak:b64_audio            -> ovos.utterance.speak.b64  (§3.4)
- speak:b64_audio.response   -> ovos.audio.speech         (§4.3)
- mycroft.audio.queue        -> ovos.audio.queue          (§4.1)
- mycroft.audio.play_sound   -> ovos.audio.play_sound     (§4.2)
- mycroft.audio.speak.status -> ovos.audio.is_speaking    (§5.3)
- mycroft.audio.speech.stop  -> ovos.audio.stop           (§6)

Tests: enum membership + legacy->spec mapping + round-trip; replace the
now-stale assertion that the AUDIO-1 members carry no migration counterpart.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
serialize() already gates the §2.1 non-emptiness rule but not the §2.1
character/whitespace rule: a topic may contain only ASCII letters, digits,
'.', ':', '_', '-' and no whitespace. Message("a b").serialize() emitted an
embedded space instead of rejecting it.

Add the §2.1 charset/whitespace validation at the serialize() wire gate
(after the non-empty check) so a malformed topic never reaches the wire,
matching the §7 producer MUST. The constructor still tolerates an empty
scaffold type for the construct-then-forward pattern.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
)

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>
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