katalyst-forge 4.2.0__tar.gz → 5.2.0__tar.gz
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.
- katalyst_forge-5.2.0/.gitignore +31 -0
- katalyst_forge-5.2.0/CHANGELOG.md +146 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/PKG-INFO +1 -1
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/pyproject.toml +6 -1
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/nodes/organizational.schema.json +4 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/__init__.py +1 -1
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/bundle.py +11 -7
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/__init__.py +57 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/batch.py +174 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/billing.py +35 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/contracts.py +128 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/core.py +385 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/dashboard.py +214 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/errors.py +122 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/generate_dashboard.py +245 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/html_report.py +383 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/money.py +43 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/protocols.py +83 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/providers/__init__.py +9 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/providers/forge_model_router.py +100 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/providers/llm_gateway.py +20 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/providers/opencode.py +269 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/redaction.py +43 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/registry.py +57 -0
- katalyst_forge-5.2.0/src/katalyst_forge/metrics/routing_policy.py +97 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/relationships.py +80 -3
- katalyst_forge-5.2.0/src/katalyst_forge/wings/_practice_refs.py +58 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/wings/design.py +5 -115
- katalyst_forge-5.2.0/src/katalyst_forge/wings/metrics.py +197 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/wings/org.py +11 -5
- katalyst_forge-5.2.0/src/katalyst_forge/wings/practices.py +565 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/wings/system.py +13 -2
- katalyst_forge-5.2.0/templates/CHANGELOG.md +671 -0
- katalyst_forge-5.2.0/tests/fake_providers_opencode.py +38 -0
- katalyst_forge-5.2.0/tests/fake_providers_router.py +31 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/expected-modified.txt +2 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/expected-new.txt +16 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/modified-inplace.patch +124 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/delivery/work_items/ric-1580-forge-model-routing.yaml +20 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/capabilities/CAP-011-per-dispatch-model-tier-routing.md +110 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/capabilities/CAP-012-dispatch-parity-and-containment.md +109 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/capabilities/CAP-013-per-stage-model-token-telemetry.md +99 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/nfrs/dispatched-stage-executes-on-requested-tier.yaml +48 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/nfrs/routed-dispatch-preserves-task-parity.yaml +46 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/nfrs/tier-resolution-fails-before-any-child-session.yaml +48 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/nfrs/zero-committed-vendor-model-identity.yaml +45 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/user-needs/UN-024-operator-selects-model-tier-per-forge-run.md +81 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/user-needs/UN-025-model-tiers-from-env-vars-no-vendor-pins.md +85 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/user-needs/UN-026-routing-tier-recorded-on-work-item.md +73 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/user-needs/UN-027-per-stage-model-and-token-telemetry.md +80 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/user-needs/UN-028-dispatch-tool-enforces-agent-allowlist.md +76 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/user-needs/UN-029-silent-or-aborted-dispatch-yields-blocked.md +83 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/user-needs/UN-030-dispatched-write-stage-stays-in-worktree.md +73 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/fixtures/incident-ric1580/new/.global/taxonomy/design/user-needs/UN-031-single-stage-spike-preserves-eleven-stage-rollout.md +76 -0
- katalyst_forge-5.2.0/tests/forge-reconcile/reconcile.test.mjs +182 -0
- katalyst_forge-5.2.0/tests/test_cost_observability_wing.py +224 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/tests/test_entry_point_discovery.py +2 -2
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/tests/test_forge.py +7 -7
- katalyst_forge-5.2.0/tests/test_metrics_batch_hygiene.py +241 -0
- katalyst_forge-5.2.0/tests/test_metrics_billing_isolation.py +112 -0
- katalyst_forge-5.2.0/tests/test_metrics_content_contract.py +307 -0
- katalyst_forge-5.2.0/tests/test_metrics_contracts.py +95 -0
- katalyst_forge-5.2.0/tests/test_metrics_dashboard.py +228 -0
- katalyst_forge-5.2.0/tests/test_metrics_dashboard_end_to_end.py +203 -0
- katalyst_forge-5.2.0/tests/test_metrics_html_report.py +182 -0
- katalyst_forge-5.2.0/tests/test_metrics_lazy_providers.py +102 -0
- katalyst_forge-5.2.0/tests/test_metrics_money_and_redaction.py +182 -0
- katalyst_forge-5.2.0/tests/test_metrics_providers.py +419 -0
- katalyst_forge-5.2.0/tests/test_metrics_resolve_compute.py +477 -0
- katalyst_forge-5.2.0/tests/test_metrics_routing_policy.py +102 -0
- katalyst_forge-5.2.0/tests/test_metrics_wing.py +64 -0
- katalyst_forge-5.2.0/tests/test_practice_assignment.py +107 -0
- katalyst_forge-5.2.0/tests/test_relationships.py +34 -0
- katalyst_forge-5.2.0/tests/test_storage_contract.py +66 -0
- katalyst_forge-5.2.0/tests/test_validate_templates_gates.py +259 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/tests/test_wing_schemas.py +15 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/tests/test_wings.py +22 -2
- katalyst_forge-5.2.0/tests/test_writer_bundle_schema.py +117 -0
- katalyst_forge-4.2.0/.gitignore +0 -8
- katalyst_forge-4.2.0/CHANGELOG.md +0 -83
- katalyst_forge-4.2.0/src/katalyst_forge/wings/practices.py +0 -216
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/LICENSE +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/CHANGELOG.md +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/README.md +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/agents/agent.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/agents/config.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/agents/manifest.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/agents/skill.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/environment.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/hooks/hook-config.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/katalyst-lock.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/nodes/common.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/nodes/design.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/nodes/portfolio.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/nodes/technical.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/schemas/taxonomy-node.schema.json +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/py.typed +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/creation/_base.yaml.j2 +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/creation/layer.yaml.j2 +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/creation/project.yaml.j2 +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/creation/stack.yaml.j2 +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/creation/system.yaml.j2 +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/creation/team.yaml.j2 +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/creation/team_member.yaml.j2 +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/.dockerignore +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/.env.example +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/README.md +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/base/Dockerfile +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/docker-compose.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/env-template/Dockerfile +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/env-template/docker-compose.override.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/env-template/environment.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/app-docker/layer.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/README.md +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/composite/terraform/main.tf +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/composite/terraform/outputs.tf +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/composite/terraform/variables.tf +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/env-template/environment.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/env-template/terragrunt.hcl +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/global/iac/context.hcl +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/global/iac/dev.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/global/iac/prod.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/global/iac/staging.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/layer.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/iac-terragrunt/version.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-argocd/README.md +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-argocd/base/app/argocd-application.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-argocd/base/kustomization.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-argocd/base/namespace.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-argocd/environment-template/argocd-application.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-argocd/environment-template/kustomization.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-argocd/environment-template/patch-argocd-application.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-argocd/layer.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/README.md +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/base/deployment.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/base/kustomization.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/base/namespace.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/base/service.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/environment-template/kustomization.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/environment-template/patch-deployment.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/global/k8s/base/kustomization.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/global/k8s/base/namespace.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/global/k8s/kustomization.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/templates/scaffolds/k8s-kustomize/layer.yaml +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/wings/__init__.py +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/wings/agentic.py +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/wings/delivery.py +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/src/katalyst_forge/wings/pipeline.py +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/tests/conftest.py +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/tests/test_agent_run.py +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/tests/test_delivery_doc_consistency.py +0 -0
- {katalyst_forge-4.2.0 → katalyst_forge-5.2.0}/tests/test_delivery_traceability.py +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
.venv/
|
|
4
|
+
dist/
|
|
5
|
+
site/
|
|
6
|
+
node_modules/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
|
|
10
|
+
# RIC-733 consistency eval run exhaust (transient, not the checked-in baseline)
|
|
11
|
+
docs/consistency-results.json
|
|
12
|
+
.prompt-cache/
|
|
13
|
+
|
|
14
|
+
# Deterministic tooling-gate run artifacts (kata-gate.sh runtime output).
|
|
15
|
+
# Real telemetry per run; regenerated on every gate invocation, not committed.
|
|
16
|
+
.global/taxonomy/delivery/agent_runs/tooling/
|
|
17
|
+
|
|
18
|
+
# Generated orientation packet (RIC-731): produced at install time, not source
|
|
19
|
+
orientation/packet.json
|
|
20
|
+
|
|
21
|
+
# Machine-local orientation fallback config (RIC-731): holds an absolute packetPath,
|
|
22
|
+
# written beside the installed contract; never source, never commit.
|
|
23
|
+
orientation-config.json
|
|
24
|
+
|
|
25
|
+
# bun.lock is the committed lockfile; npm's is not
|
|
26
|
+
package-lock.json
|
|
27
|
+
|
|
28
|
+
# Watchdog runtime state (RIC-1592): markers and ledger entries are produced at runtime
|
|
29
|
+
# outside any repo, under KATALYST_WATCHDOG_STATE_DIR. Ignored so a stray write can never
|
|
30
|
+
# pollute the working tree that RIC-1591 has to diff cleanly.
|
|
31
|
+
.katalyst-watchdog/
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Changelog — katalyst-forge
|
|
2
|
+
|
|
3
|
+
All notable changes to the **katalyst-forge** bundle are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
As of 4.2.0 this package lives in its own repository
|
|
9
|
+
(`esimplicityinc/katalyst-forge`) and tags use the plain `vX.Y.Z` prefix.
|
|
10
|
+
Releases 4.1.0 and earlier were cut from the `katalyst-taxonomy` monorepo
|
|
11
|
+
with `forge-vX.Y.Z` tags.
|
|
12
|
+
|
|
13
|
+
The Forge **bundle `schema_version`** (declared in `bundle.py`) is the
|
|
14
|
+
consumer-facing node-model contract and is versioned separately from this
|
|
15
|
+
PyPI package version. A breaking node-type/model change bumps `schema_version`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [5.2.0] -- 2026-08-08
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Cost observability as metrics-wing content** (RIC-1590). Run cost is modelled as `metric_definition` plus `signal_source` content rather than bespoke reporting code. Ships `src/katalyst_forge/metrics/` with a pure computation core (no I/O), a deny-by-default projection into `CanonicalSignalBatch`, `SignalProvider` / `PriceCatalogue` / `RoutingPolicy` protocols, lazily resolved providers for OpenCode and forge-model-router, a text dashboard, an HTML report, and the `cost-observability` template bundle with its global action and Just recipe. Metered and subscription-notional spend are kept apart by construction: `MetricResult` is keyed by billing mode and offers no total, and `__add__`, `__radd__` and `__float__` raise.
|
|
24
|
+
- **Routing comparison policy** (RIC-1616). `routing-headroom` and `routing-savings-realized` can now produce a figure. The comparison models come from the environment (`FORGE_COST_ORCHESTRATOR_MODEL`, `FORGE_COST_TARGET_MODEL`, `FORGE_COST_TARGET_MODEL_<STAGE>`), so no model identifier is hard-coded in code or content. An unconfigured environment yields no policy, leaving the previous behaviour of absent figures rather than zeros.
|
|
25
|
+
- **`optional_fields` on a signal source** (RIC-1617). A provider-synthesized value that may legitimately be absent, such as a Forge stage inferred from a session that is not a Forge stage, no longer discards the whole batch.
|
|
26
|
+
- **Bundle content and non-ownership in the writer reference** (RIC-1621). `kata-tax-writer` now has schema entries for template-bundle content, and explicitly disclaims `tests/`, `src/`, `scripts/` and `.github/workflows/`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **The metered cost path never returned a record** (RIC-1615). The OpenCode adapter read its root sessions from a `query_config` key that only ever existed in a test fixture, so every real run walked an empty session list. Run identity is now an explicit argument on `SignalProvider.fetch`, interpreted per adapter.
|
|
31
|
+
- **The session walk dropped spend it should have counted** (RIC-1615). It began at the root's children, so an orchestrator's own tokens went uncounted and a single-session run reported nothing, and it descended only one level even though the children endpoint returns direct children only. It now yields the root and every transitive descendant once each.
|
|
32
|
+
- **Router telemetry was rejected outright** (RIC-1615). A real run writes `cost_usd` as a JSON number; decoding without `parse_float=Decimal` lost the exactness `parse_money` requires and aborted the dashboard.
|
|
33
|
+
- **The price catalogue was built eagerly** (RIC-1615), demanding OpenCode credentials from users who only wanted router figures. It is now built on first price lookup.
|
|
34
|
+
- **`routing-headroom` reported a fabricated zero** (RIC-1616) when no record had a resolvable comparison target. A rendered `$0.00` reads as routing already being optimal rather than as routing never having been measured, so it now yields no figure unless a comparison was made.
|
|
35
|
+
- **One uncomputable metric aborted the whole report** (RIC-1616). `compute` is now isolated per metric, matching the per-source isolation already in place.
|
|
36
|
+
- **The action schema documentation did not match the loader** (RIC-1619). `katalyst-action-builder` documented `kind: Action` on an unrecognised apiVersion with `spec.type`, `spec.timeoutSeconds` and `spec.tags`; following it produced zero actions and, for the kind and apiVersion errors, zero errors as well.
|
|
37
|
+
- **The writer's Kind Mapping carried the same defect** (RIC-1621). `plugin-action` mapped to `kind: Action` on `katalyst.taxonomy.plugins.actions/v1alpha`, so the single writer would have produced action files that no loader reads.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- CI now runs `ruff format --check` over `src`, `tests` and `scripts` (RIC-1620), with the pre-existing drift reformatted first so the gate did not turn open branches red.
|
|
42
|
+
|
|
43
|
+
## [5.1.0] -- 2026-07-28
|
|
44
|
+
|
|
45
|
+
- **Canonical ADR/NFR storage** (RIC-1358) — ADR and NFR definitions now declare flat identity-derived storage under `.global/taxonomy/design/adrs/` and `.global/taxonomy/design/nfrs/`. Forge agents, skills, templates, fixtures, and documentation use the plural directories; legacy multi-document examples are labeled explicitly.
|
|
46
|
+
- Migrated the nine Forge-owned NFR artifacts from `design/nfr/` to `design/nfrs/` and added `NFR-010` to require executable storage consumers to resolve registry metadata instead of hard-coding artifact kinds or paths.
|
|
47
|
+
|
|
48
|
+
## [5.0.0] -- 2026-07-22
|
|
49
|
+
|
|
50
|
+
Bundle `schema_version` bumped to **5** and manifest version to **5.0** to reflect breaking relationship renames and parent-rule changes below.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **`practice` node kind** in the Practices wing (RIC-1273). A bundle node that sits between `practice_area` and standards/methods/competencies/tools, enabling strict nesting: `practice_area` → `practice` → `{standard, method, competency, tool}`. Practices are assignable units that reference (do not own) standards and methods.
|
|
55
|
+
- **`INCLUDES_PRACTICE` relationship** (`practice_area` → `practice`): grouping edge for the new strict nesting hierarchy.
|
|
56
|
+
- **`PRESCRIBES_STANDARD` / `PRESCRIBES_METHOD` relationships** (`practice` → `standard` / `method`): practice prescribes which standards and methods it bundles.
|
|
57
|
+
- **`ADHERES_TO_PRACTICE` / `ADHERES_TO_STANDARD` / `ADHERES_TO_METHOD` relationships**: assignment edges allowing consumer nodes (system, stack, layer, library, tool, organization, project, team) to declare which practices, standards, and methods they follow.
|
|
58
|
+
- **Practice assignment fields** (`practices`, `standards`, `methods`) spliced into 8 consumer ("follower") node kinds via shared `_practice_refs.py` helper.
|
|
59
|
+
- **`metric_definition`, `signal_source`, `threshold_profile` node kinds** in the new Metrics wing (RIC-1272). Brings the bundle to **36 node types across 8 wings**.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **BREAKING: `competency` re-parented from `practice_area` to `practice`** (RIC-1273). The `ParentRule.allowed_parent_kinds` changed from `{"practice_area"}` to `{"practice"}`. Any existing `competency` instances parented under a `practice_area` must be re-parented under a `practice` node. No known consuming taxonomies have competency instances.
|
|
64
|
+
- **BREAKING: Relationship renames** (RIC-1273). The following relationship types were renamed for naming consistency (all edge families now use verb-first, actor-centric naming):
|
|
65
|
+
- `INCLUDES_METHOD` / `INCLUDES_STANDARD` → removed (replaced by `PRESCRIBES_STANDARD` / `PRESCRIBES_METHOD` on the new `practice` node)
|
|
66
|
+
- Assignment family unified: `FOLLOWS_PRACTICE` → `ADHERES_TO_PRACTICE`, `APPLIES_METHOD` → `ADHERES_TO_METHOD` (`ADHERES_TO_STANDARD` unchanged)
|
|
67
|
+
- **`standard` and `method` relocated from Design wing to Practices wing** (RIC-1273). `nfr` is now a child of `standard`; `adr` is now a child of `method`. Design wing shrinks from 8 → 6 kinds; Practices wing grows from 4 → 9 kinds.
|
|
68
|
+
|
|
69
|
+
### Migration
|
|
70
|
+
|
|
71
|
+
Consuming taxonomies that reference the removed/renamed relationship types in cross-references or queries must update:
|
|
72
|
+
|
|
73
|
+
| Old | New |
|
|
74
|
+
|-----|-----|
|
|
75
|
+
| `INCLUDES_METHOD` | `PRESCRIBES_METHOD` (on `practice`, not `practice_area`) |
|
|
76
|
+
| `INCLUDES_STANDARD` | `PRESCRIBES_STANDARD` (on `practice`, not `practice_area`) |
|
|
77
|
+
| `FOLLOWS_PRACTICE` | `ADHERES_TO_PRACTICE` |
|
|
78
|
+
| `APPLIES_METHOD` | `ADHERES_TO_METHOD` |
|
|
79
|
+
| `competency` parented under `practice_area` | Re-parent under `practice` |
|
|
80
|
+
| `nfr` / `adr` in design wing | Now in practices wing (no YAML change needed; wing is metadata) |
|
|
81
|
+
|
|
82
|
+
## [4.2.0] -- 2026-07-06
|
|
83
|
+
|
|
84
|
+
Repo split (RIC-1070). First release from the standalone
|
|
85
|
+
`esimplicityinc/katalyst-forge` repository, extracted from `katalyst-taxonomy`.
|
|
86
|
+
No node-model changes; bundle `schema_version` stays **4**.
|
|
87
|
+
|
|
88
|
+
### Added
|
|
89
|
+
|
|
90
|
+
- Standalone repository: the Forge schema bundle, the 21 kata-forge agent
|
|
91
|
+
definitions and forge skills (extracted from `katalyst-taxonomy-agents`),
|
|
92
|
+
the creation/scaffold templates, the JSON Schemas, and the Forge lifecycle
|
|
93
|
+
docs now live and version together as one product.
|
|
94
|
+
- JSON Schemas ship as package data in the wheel under `katalyst_forge/schemas/`.
|
|
95
|
+
|
|
96
|
+
### Changed
|
|
97
|
+
|
|
98
|
+
- Release tags drop the `forge-` prefix: this repo tags plain `vX.Y.Z`.
|
|
99
|
+
- Docs move to https://esimplicityinc.github.io/katalyst-forge/.
|
|
100
|
+
|
|
101
|
+
## [4.1.0] -- 2026-06-26
|
|
102
|
+
|
|
103
|
+
Delivery-wing evolution (#138 / #140). Bundle `schema_version` stays **4** (additive within the v4 node-model); bundle node count 29 → 30.
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
|
|
107
|
+
- **`agent_run` node kind** (child of `work_item`): per-stage agent telemetry (`stages`, `totals`, `entered_status` / `exited_status`, `started` / `ended`) -- identifiers and counts only, no prompts or PII.
|
|
108
|
+
- **`work_item.status` 10-state lifecycle enum** (`idea, proposed, accepted, planned, in_progress, blocked, delivered, validated, released, retired`), enforced through the engine's `FieldSpec.allowed_values`.
|
|
109
|
+
- **Single `TRACES_TO` relationship** on `work_item`, disambiguated by the source field's target kind, covering `capabilities`, `user_needs`, `nfrs`, `adrs`, `design_specs`, `specifications`. Adding a new traceable target is a one-line field addition.
|
|
110
|
+
- **`change_entry` -> `work_item` link** (`work_item_id` + `REFERENCES_WORK_ITEM`).
|
|
111
|
+
|
|
112
|
+
### Changed
|
|
113
|
+
|
|
114
|
+
- Minimum `katalyst-engine` raised to `>=4.1` -- the `work_item` status enum relies on `FieldSpec.allowed_values`, added in engine 4.1.0.
|
|
115
|
+
- Replaced the per-target relationship definitions (`implements_capability`, `addresses_need`, `satisfies_nfr`) with the single `TRACES_TO` edge. `road_id` / `REFERENCES_ROAD_ITEM` on `change_entry` retained as a deprecated alias of `work_item_id`.
|
|
116
|
+
|
|
117
|
+
## [4.0.0] -- 2026-06-16
|
|
118
|
+
|
|
119
|
+
Bundle `schema_version` bumped to **4** and manifest version to **4.0** to reflect the breaking node-kind removals below.
|
|
120
|
+
|
|
121
|
+
### Removed
|
|
122
|
+
|
|
123
|
+
- **BREAKING: Relocated mechanism kinds out of the Forge bundle** (#79/#82). Six node kinds no longer ship in Forge: `template`, `action`, `template_overlay`, `dx`, `cicd_job_template`, and `cicd_workflow_template`. Scaffolding/CI-CD mechanisms are now provided by the taxonomy runtime and its plugins rather than declared as Forge node types. Documents using these kinds must migrate (`kata tax migrate`); `template_overlay` becomes `node_wiring`.
|
|
124
|
+
|
|
125
|
+
### Added
|
|
126
|
+
|
|
127
|
+
- **`design_spec` node type** in the Design wing (#44). A pre-executable specification artifact sitting between `user_need` and `specification` in the intent chain, capturing visual structure, interaction flows, and testable design constraints. Includes a `design_specs` field + `IMPLEMENTS_DESIGN` cross-reference on `specification`, and the `DESIGNS_FOR_NEED` / `DESIGNS_CAPABILITY` / `IMPLEMENTS_DESIGN` relationships. Brings the Forge bundle to **29 node types**.
|
|
128
|
+
|
|
129
|
+
## [3.0.1] -- 2026-06-11
|
|
130
|
+
|
|
131
|
+
### Changed
|
|
132
|
+
- **Corrected bundle `schema_version` to `3`** to reflect the breaking `user_story` → `user_need` rename that shipped in 3.0.0. The `BundleManifest.version` was bumped to `3.0` and `SchemaDefinition.schema_version` to `3`.
|
|
133
|
+
- First release published independently via the per-package `forge-v*` release pipeline.
|
|
134
|
+
|
|
135
|
+
## [3.0.0] -- 2026-06-10
|
|
136
|
+
|
|
137
|
+
### Changed
|
|
138
|
+
- **BREAKING: Renamed `user_story` node type to `user_need`** in the Design wing. The kind (`user_story` → `user_need`), directory (`user_stories/` → `user_needs/`), display labels (`User Story`/`User Stories` → `User Need`/`User Needs`), and Neo4j label (`UserStory` → `UserNeed`) all changed. Creation template `user_story.yaml.j2` → `user_need.yaml.j2`. No backward-compatibility alias is provided. (RIC-616)
|
|
139
|
+
|
|
140
|
+
### Added
|
|
141
|
+
- **`katalyst-xbrl` bundle** companion node types and the pluggable parser registry landed in the same `3.0.0` release train (see root `CHANGELOG.md` for cross-package detail).
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
> Versions prior to 3.0.0 were released in lockstep with `katalyst-taxonomy`
|
|
146
|
+
> under the umbrella `vX.Y.Z` tags. See the root `CHANGELOG.md` for that history.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "katalyst-forge"
|
|
3
|
-
version = "
|
|
3
|
+
version = "5.2.0"
|
|
4
4
|
description = "Katalyst Forge: the delivery-lifecycle bundle (node kinds, wings, agents, templates, schemas)"
|
|
5
5
|
license = { text = "Proprietary" }
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -66,6 +66,11 @@ dev = [
|
|
|
66
66
|
# test_wing_schemas.py validates the shipped JSON Schemas
|
|
67
67
|
"jsonschema>=4.22",
|
|
68
68
|
"referencing>=0.35",
|
|
69
|
+
# test_validate_templates_gates.py loads scripts/validate-templates.py, which
|
|
70
|
+
# imports these. CI's templates-validate job installs them ad hoc; the pytest
|
|
71
|
+
# job needs them in its own environment to exercise the gate logic.
|
|
72
|
+
"pyyaml>=6",
|
|
73
|
+
"jinja2>=3.1",
|
|
69
74
|
]
|
|
70
75
|
|
|
71
76
|
[tool.ruff]
|
|
@@ -96,6 +96,10 @@
|
|
|
96
96
|
"type": "string",
|
|
97
97
|
"minLength": 1,
|
|
98
98
|
"description": "Human-readable display name for the competency (e.g. 'Kubernetes Administration')."
|
|
99
|
+
},
|
|
100
|
+
"competency_type": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"description": "Optional free-form kind of competency: practice, system, or tools. No allowed_values enum is enforced."
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""katalyst-forge — The Forge bundle for katalyst-engine.
|
|
2
2
|
|
|
3
|
-
Provides
|
|
3
|
+
Provides 36 node-type definitions across 8 wings for software delivery taxonomy.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Forge bundle — the pre-installed taxonomy for software delivery.
|
|
2
2
|
|
|
3
|
-
Assembles
|
|
3
|
+
Assembles 36 node types across 8 wings from the wing modules into
|
|
4
4
|
a single ``ForgeBundle`` that implements the engine's ``Bundle``
|
|
5
5
|
protocol. Discovered via the ``katalyst_engine.bundles`` entry point.
|
|
6
6
|
|
|
@@ -9,12 +9,13 @@ protocol. Discovered via the ``katalyst_engine.bundles`` entry point.
|
|
|
9
9
|
Wing modules:
|
|
10
10
|
- ``wings/system.py`` — system, environment, stack, layer, library, capability
|
|
11
11
|
- ``wings/org.py`` — organization, program, project, team, team_member, role
|
|
12
|
-
- ``wings/practices.py``
|
|
13
|
-
- ``wings/design.py`` — business_domain_model, user_type, user_need, design_spec,
|
|
12
|
+
- ``wings/practices.py`` — practice_area, practice, competency, skill, standard, nfr, method, adr, tool
|
|
13
|
+
- ``wings/design.py`` — business_domain_model, user_type, user_need, design_spec, specification, assertion
|
|
14
14
|
- ``wings/delivery.py`` — work_item, change_entry, agent_run
|
|
15
15
|
- ``wings/pipeline.py`` — cicd_stage
|
|
16
16
|
- ``wings/pipeline.py`` — cicd_stage, cicd_job_template, cicd_workflow_template
|
|
17
17
|
- ``wings/agentic.py`` — agent, agent_skill
|
|
18
|
+
- ``wings/metrics.py`` — metric_definition, signal_source, threshold_profile
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
21
|
from __future__ import annotations
|
|
@@ -36,6 +37,7 @@ from katalyst_forge.relationships import ALL_RELATIONSHIP_DEFINITIONS
|
|
|
36
37
|
from katalyst_forge.wings.agentic import AGENTIC_WING, AGENTIC_WING_NODES
|
|
37
38
|
from katalyst_forge.wings.delivery import DELIVERY_WING, DELIVERY_WING_NODES
|
|
38
39
|
from katalyst_forge.wings.design import DESIGN_WING, DESIGN_WING_NODES
|
|
40
|
+
from katalyst_forge.wings.metrics import METRICS_WING, METRICS_WING_NODES
|
|
39
41
|
from katalyst_forge.wings.org import ORG_WING, ORG_WING_NODES
|
|
40
42
|
from katalyst_forge.wings.pipeline import PIPELINE_WING, PIPELINE_WING_NODES
|
|
41
43
|
from katalyst_forge.wings.practices import PRACTICES_WING, PRACTICES_WING_NODES
|
|
@@ -53,6 +55,7 @@ ALL_NODE_DEFINITIONS: list[NodeDefinition] = [
|
|
|
53
55
|
*DELIVERY_WING_NODES,
|
|
54
56
|
*PIPELINE_WING_NODES,
|
|
55
57
|
*AGENTIC_WING_NODES,
|
|
58
|
+
*METRICS_WING_NODES,
|
|
56
59
|
]
|
|
57
60
|
|
|
58
61
|
ALL_KINDS: frozenset[str] = frozenset(nd.describes_kind for nd in ALL_NODE_DEFINITIONS)
|
|
@@ -65,6 +68,7 @@ ALL_WING_DEFINITIONS: list[WingDefinition] = [
|
|
|
65
68
|
PRACTICES_WING,
|
|
66
69
|
PIPELINE_WING,
|
|
67
70
|
AGENTIC_WING,
|
|
71
|
+
METRICS_WING,
|
|
68
72
|
]
|
|
69
73
|
|
|
70
74
|
ALL_WING_NAMES: frozenset[str] = frozenset(wd.identity.name for wd in ALL_WING_DEFINITIONS)
|
|
@@ -76,7 +80,7 @@ ALL_WING_NAMES: frozenset[str] = frozenset(wd.identity.name for wd in ALL_WING_D
|
|
|
76
80
|
|
|
77
81
|
|
|
78
82
|
class ForgeBundle:
|
|
79
|
-
"""The Forge bundle —
|
|
83
|
+
"""The Forge bundle — 36 node types across 8 wings.
|
|
80
84
|
|
|
81
85
|
This is the pre-installed taxonomy bundle that ships with
|
|
82
86
|
Katalyst Taxonomy. It models software delivery organizations
|
|
@@ -89,10 +93,10 @@ class ForgeBundle:
|
|
|
89
93
|
def manifest(self) -> BundleManifest:
|
|
90
94
|
return BundleManifest(
|
|
91
95
|
identity=Identity(kind="bundle", name="forge"),
|
|
92
|
-
version=Version(major=
|
|
96
|
+
version=Version(major=5, minor=1),
|
|
93
97
|
provided_kinds=ALL_KINDS,
|
|
94
98
|
wing_names=ALL_WING_NAMES,
|
|
95
|
-
description="Forge taxonomy —
|
|
99
|
+
description="Forge taxonomy — 36 node types for modeling software delivery organizations",
|
|
96
100
|
)
|
|
97
101
|
|
|
98
102
|
def node_definitions(self) -> list[NodeDefinition]:
|
|
@@ -109,7 +113,7 @@ class ForgeBundle:
|
|
|
109
113
|
identity=Identity(kind="schema", name="forge"),
|
|
110
114
|
describes_kind="taxonomy",
|
|
111
115
|
api_version="v2alpha",
|
|
112
|
-
schema_version=Version(major=
|
|
116
|
+
schema_version=Version(major=5),
|
|
113
117
|
node_definitions=tuple(nd.identity for nd in ALL_NODE_DEFINITIONS),
|
|
114
118
|
wing_definitions=tuple(wd.identity for wd in ALL_WING_DEFINITIONS),
|
|
115
119
|
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Public API for the cost/observability metrics adapter.
|
|
2
|
+
|
|
3
|
+
Must NOT import anything from .providers, which would defeat the registry's
|
|
4
|
+
lazy provider resolution (see registry.py and providers/__init__.py).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from katalyst_forge.metrics.batch import CanonicalSignalBatch, project_batch
|
|
10
|
+
from katalyst_forge.metrics.billing import BillingMode
|
|
11
|
+
from katalyst_forge.metrics.core import MetricResult, compute, resolve
|
|
12
|
+
from katalyst_forge.metrics.errors import (
|
|
13
|
+
BatchConstructionError,
|
|
14
|
+
ContractViolationError,
|
|
15
|
+
CrossBillingModeError,
|
|
16
|
+
MetricsAdapterError,
|
|
17
|
+
MissingCredentialError,
|
|
18
|
+
MissingMappedFieldError,
|
|
19
|
+
MoneyPrecisionError,
|
|
20
|
+
PriceUnavailableError,
|
|
21
|
+
ProviderPayloadError,
|
|
22
|
+
ProviderUnavailableError,
|
|
23
|
+
UnknownBillingModeError,
|
|
24
|
+
UnknownProviderError,
|
|
25
|
+
UnmappedFieldError,
|
|
26
|
+
)
|
|
27
|
+
from katalyst_forge.metrics.money import parse_money
|
|
28
|
+
from katalyst_forge.metrics.protocols import PriceCatalogue, SignalProvider
|
|
29
|
+
from katalyst_forge.metrics.redaction import redact
|
|
30
|
+
from katalyst_forge.metrics.registry import SignalProviderRegistry
|
|
31
|
+
|
|
32
|
+
__all__ = (
|
|
33
|
+
"BillingMode",
|
|
34
|
+
"CanonicalSignalBatch",
|
|
35
|
+
"project_batch",
|
|
36
|
+
"MetricResult",
|
|
37
|
+
"resolve",
|
|
38
|
+
"compute",
|
|
39
|
+
"SignalProviderRegistry",
|
|
40
|
+
"SignalProvider",
|
|
41
|
+
"PriceCatalogue",
|
|
42
|
+
"parse_money",
|
|
43
|
+
"redact",
|
|
44
|
+
"MetricsAdapterError",
|
|
45
|
+
"BatchConstructionError",
|
|
46
|
+
"UnmappedFieldError",
|
|
47
|
+
"MissingMappedFieldError",
|
|
48
|
+
"ContractViolationError",
|
|
49
|
+
"MoneyPrecisionError",
|
|
50
|
+
"PriceUnavailableError",
|
|
51
|
+
"UnknownBillingModeError",
|
|
52
|
+
"CrossBillingModeError",
|
|
53
|
+
"UnknownProviderError",
|
|
54
|
+
"MissingCredentialError",
|
|
55
|
+
"ProviderUnavailableError",
|
|
56
|
+
"ProviderPayloadError",
|
|
57
|
+
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Canonical signal batch: deny-by-default projection and a non-bypassable type.
|
|
2
|
+
|
|
3
|
+
CanonicalSignalBatch can only be produced by project_batch. The constructor
|
|
4
|
+
requires a private guard sentinel that lives only in this module, so no
|
|
5
|
+
outside code, including copy.copy, copy.deepcopy, and pickle, can forge or
|
|
6
|
+
clone an instance. Projection itself is deny-by-default: only canonical
|
|
7
|
+
fields named in a source's field_mapping are ever copied out of a provider
|
|
8
|
+
record, so any field a provider returns but nobody mapped (including
|
|
9
|
+
prompt-shaped fields such as "instructions" or "system_prompt") is dropped,
|
|
10
|
+
never passed through.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Mapping, Sequence
|
|
16
|
+
from types import MappingProxyType
|
|
17
|
+
from typing import Final, cast
|
|
18
|
+
|
|
19
|
+
from katalyst_forge.metrics.billing import BillingMode
|
|
20
|
+
from katalyst_forge.metrics.errors import BatchConstructionError, MissingMappedFieldError
|
|
21
|
+
|
|
22
|
+
__all__ = ("CanonicalSignalBatch", "project_batch")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _ProjectionGuard:
|
|
26
|
+
"""Sentinel type; only _GUARD below is a valid instance of it."""
|
|
27
|
+
|
|
28
|
+
__slots__ = ()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_GUARD: Final[_ProjectionGuard] = _ProjectionGuard()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CanonicalSignalBatch:
|
|
35
|
+
"""An immutable, already-projected batch of canonical signal records."""
|
|
36
|
+
|
|
37
|
+
__slots__ = ("_billing_mode", "_run_id", "_source_name", "_records")
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
_guard: _ProjectionGuard,
|
|
42
|
+
*,
|
|
43
|
+
billing_mode: BillingMode,
|
|
44
|
+
run_id: str,
|
|
45
|
+
source_name: str,
|
|
46
|
+
records: tuple[Mapping[str, object], ...],
|
|
47
|
+
) -> None:
|
|
48
|
+
if _guard is not _GUARD:
|
|
49
|
+
raise BatchConstructionError("only project_batch may construct a CanonicalSignalBatch")
|
|
50
|
+
self._billing_mode = billing_mode
|
|
51
|
+
self._run_id = run_id
|
|
52
|
+
self._source_name = source_name
|
|
53
|
+
self._records = records
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def billing_mode(self) -> BillingMode:
|
|
57
|
+
return self._billing_mode
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def run_id(self) -> str:
|
|
61
|
+
return self._run_id
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def source_name(self) -> str:
|
|
65
|
+
return self._source_name
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def records(self) -> tuple[Mapping[str, object], ...]:
|
|
69
|
+
return self._records
|
|
70
|
+
|
|
71
|
+
def __copy__(self) -> CanonicalSignalBatch:
|
|
72
|
+
raise BatchConstructionError("CanonicalSignalBatch cannot be copied")
|
|
73
|
+
|
|
74
|
+
def __deepcopy__(self, memo: dict[int, object]) -> CanonicalSignalBatch:
|
|
75
|
+
raise BatchConstructionError("CanonicalSignalBatch cannot be deepcopied")
|
|
76
|
+
|
|
77
|
+
def __reduce__(self) -> tuple[object, ...]:
|
|
78
|
+
raise BatchConstructionError("CanonicalSignalBatch cannot be pickled")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _as_mapping(value: object) -> Mapping[str, object] | None:
|
|
82
|
+
"""Narrow value to a concretely-typed Mapping, or None if it is not one."""
|
|
83
|
+
if isinstance(value, Mapping):
|
|
84
|
+
return cast("Mapping[str, object]", value)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_mapping(doc: Mapping[str, object], key: str) -> Mapping[str, object]:
|
|
89
|
+
"""Read doc[key] as a Mapping, defaulting to empty rather than raising."""
|
|
90
|
+
mapped = _as_mapping(doc.get(key))
|
|
91
|
+
return mapped if mapped is not None else {}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _str_frozenset(value: object) -> frozenset[str]:
|
|
95
|
+
"""Read a YAML string list as a set, tolerating a bare string or absence."""
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
return frozenset({value})
|
|
98
|
+
if isinstance(value, (list, tuple)):
|
|
99
|
+
items = cast("Sequence[object]", value)
|
|
100
|
+
return frozenset(item for item in items if isinstance(item, str))
|
|
101
|
+
return frozenset()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _read_path(record: Mapping[str, object], path: str) -> object | None:
|
|
105
|
+
"""Walk a dotted path through record, returning None on any non-Mapping hop."""
|
|
106
|
+
current: object = record
|
|
107
|
+
for segment in path.split("."):
|
|
108
|
+
current_mapping = _as_mapping(current)
|
|
109
|
+
if current_mapping is None:
|
|
110
|
+
return None
|
|
111
|
+
current = current_mapping.get(segment)
|
|
112
|
+
return current
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def project_batch(
|
|
116
|
+
*,
|
|
117
|
+
signal_source_doc: Mapping[str, object],
|
|
118
|
+
records: Sequence[Mapping[str, object]],
|
|
119
|
+
billing_mode: BillingMode,
|
|
120
|
+
run_id: str,
|
|
121
|
+
source_name: str,
|
|
122
|
+
) -> CanonicalSignalBatch:
|
|
123
|
+
"""Deny-by-default projection: keep only canonical fields named in field_mapping.
|
|
124
|
+
|
|
125
|
+
field_mapping is canonical_input_name -> provider.dotted.path. For every
|
|
126
|
+
provider record, a new dict is built containing only those canonical
|
|
127
|
+
keys, each pulled from the mapped path. A field the provider returns but
|
|
128
|
+
no mapping references is never copied. A mapped path missing from one
|
|
129
|
+
record is simply omitted from that record's projection; a mapping
|
|
130
|
+
that resolves for zero records across the whole batch is an error, since
|
|
131
|
+
that indicates the mapping itself is wrong rather than sparse data, unless
|
|
132
|
+
the source names the field in optional_fields.
|
|
133
|
+
|
|
134
|
+
optional_fields exists for a value the provider synthesizes and may
|
|
135
|
+
legitimately not have, such as a Forge stage inferred from a session that is
|
|
136
|
+
not a Forge stage at all. Holding such a field to the zero-resolution guard
|
|
137
|
+
discarded a run's entire cost rather than only its stage attribution, so a
|
|
138
|
+
total vanished because one attribution dimension was absent. The trade is
|
|
139
|
+
that a typo in an optional field's path stops being caught, which is why it
|
|
140
|
+
is declared per source rather than assumed.
|
|
141
|
+
"""
|
|
142
|
+
spec = _get_mapping(signal_source_doc, "spec")
|
|
143
|
+
field_mapping = _get_mapping(spec, "field_mapping")
|
|
144
|
+
optional_fields = _str_frozenset(spec.get("optional_fields"))
|
|
145
|
+
|
|
146
|
+
canonical_paths: tuple[tuple[str, str], ...] = tuple(
|
|
147
|
+
(canonical_key, path)
|
|
148
|
+
for canonical_key, path in field_mapping.items()
|
|
149
|
+
if isinstance(path, str)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
resolved_counts: dict[str, int] = {canonical_key: 0 for canonical_key, _ in canonical_paths}
|
|
153
|
+
projected_records: list[Mapping[str, object]] = []
|
|
154
|
+
|
|
155
|
+
for record in records:
|
|
156
|
+
projected: dict[str, object] = {}
|
|
157
|
+
for canonical_key, path in canonical_paths:
|
|
158
|
+
value = _read_path(record, path)
|
|
159
|
+
if value is not None:
|
|
160
|
+
projected[canonical_key] = value
|
|
161
|
+
resolved_counts[canonical_key] += 1
|
|
162
|
+
projected_records.append(MappingProxyType(projected))
|
|
163
|
+
|
|
164
|
+
for canonical_key, count in resolved_counts.items():
|
|
165
|
+
if count == 0 and canonical_key not in optional_fields:
|
|
166
|
+
raise MissingMappedFieldError(canonical_field=canonical_key, source_name=source_name)
|
|
167
|
+
|
|
168
|
+
return CanonicalSignalBatch(
|
|
169
|
+
_GUARD,
|
|
170
|
+
billing_mode=billing_mode,
|
|
171
|
+
run_id=run_id,
|
|
172
|
+
source_name=source_name,
|
|
173
|
+
records=tuple(projected_records),
|
|
174
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Billing mode enum and strict parsing (metered vs. subscription-notional)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
from katalyst_forge.metrics.errors import UnknownBillingModeError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BillingMode(StrEnum):
|
|
11
|
+
"""The two ways a signal source's cost figures are billed.
|
|
12
|
+
|
|
13
|
+
METERED means the provider charges per unit consumed. SUBSCRIPTION_NOTIONAL
|
|
14
|
+
means the spend is a flat subscription and the figure is a notional
|
|
15
|
+
allocation, not an invoice line. These are different quantities and must
|
|
16
|
+
never be combined (see core.MetricResult).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
METERED = "metered"
|
|
20
|
+
SUBSCRIPTION_NOTIONAL = "subscription_notional"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_billing_mode(raw: object) -> BillingMode:
|
|
24
|
+
"""Parse a billing mode from a document value, never defaulting.
|
|
25
|
+
|
|
26
|
+
A missing or unrecognized billing mode is a contract error, not a hint to
|
|
27
|
+
guess metered as a safe default; guessing wrong silently mislabels cost.
|
|
28
|
+
"""
|
|
29
|
+
if isinstance(raw, BillingMode):
|
|
30
|
+
return raw
|
|
31
|
+
if isinstance(raw, str):
|
|
32
|
+
for mode in BillingMode:
|
|
33
|
+
if mode.value == raw:
|
|
34
|
+
return mode
|
|
35
|
+
raise UnknownBillingModeError(raw)
|