Commit ec2aab4a authored by Christos Tranoris's avatar Christos Tranoris
Browse files

resource changes

parent d4c44a00
Loading
Loading
Loading
Loading
+15 −5
Changes for README.md: 15 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -100,8 +100,11 @@ public class VirtualMachine extends ExampleResourceSpec {

    @Override
    public ResourceUpdate resourceLogic(ResourceUpdate ru, String resourceId, boolean simulation) {
        // read characteristics, do the work (unless simulation), report status
        // read characteristics, do the work (unless simulation), then report health
        ru.setResourceStatus(ResourceStatusType.AVAILABLE);
        ru.setOperationalState(ResourceOperationalStateType.ENABLE);
        ru.setAdministrativeState(ResourceAdministrativeStateType.UNLOCKED);
        ru.setUsageState(ResourceUsageStateType.ACTIVE);
        return ru;
    }
}
@@ -120,15 +123,22 @@ logical.
| `org.etsi.osl.operationDurationSeconds` | Planning only: the window to size the operation estimate against |
| `X-OSL-SIMULATION` | Present ⇒ dry-run; suppress all side effects |

On the **production** path you must update the resource back through the TMF API and set a terminal
status — at least `AVAILABLE`, or `TERMINATED` on error. Nothing else ends OSOM's wait loop.
On the **production** path you must update the resource back through the TMF API and declare the
resource's **health** — the ITU-T X.731 pair `operationalState` + `administrativeState`. OSOM folds
them into a verdict, and that verdict, *not* `resourceStatus`, is what moves the owning service:
`ENABLE`+`UNLOCKED` → UP → the service reaches ACTIVE, `DISABLE`+`UNLOCKED` → DOWN, anything
+`SHUTDOWN` → GONE → TERMINATED. Leave either attribute unset and health reads PENDING, which moves
nothing: the order never fails, it simply never finishes. `resourceStatus` is pool bookkeeping and ends
no wait loop on its own. See
[the authoring guide](docs/20260719_0017_writing-an-openslice-controller.md) — *Health, not status*.

## Simulation and cost estimates

On the planning path the controller runs the real spec logic (so the tentative plan reflects what the
controller actually computes), then reports to OSOM's `PLANNING.RESOURCE.CREATEORUPDATE` callback queue
— first `RESERVED`, then `AVAILABLE` after a simulated deploy delay, so OSOM observes a realistic
progression and converges.
— first HELD (`RESERVED` + `ENABLE`/`LOCKED`), then UP (`AVAILABLE` + `ENABLE`/`UNLOCKED`) after a
simulated deploy delay, so OSOM observes a realistic progression and converges. The health attributes
travel as `org.etsi.osl.operationalState` / `administrativeState` / `usageState` headers.

Each callback carries the estimate for its phase as headers:

+97 −26
Changes for docs/20260719_0017_writing-an-openslice-controller.md: 97 added lines, 26 removed lines.
Original line number Diff line number Diff line
@@ -144,9 +144,9 @@ sequenceDiagram
    Q->>C: createResource(headers, body)
    C->>C: registry.get(specName)
    C->>S: resourceLogic(ru, resourceId, simulation=false)
    S-->>C: status.* characteristics, status = AVAILABLE
    S-->>C: status.* characteristics,<br/>AVAILABLE + ENABLE/UNLOCKED/ACTIVE
    C->>TMF: updateResourceById(resourceId, ru)
    TMF-->>OSOM: resource now AVAILABLE → wait loop converges
    TMF-->>OSOM: health UP → supporting service reaches ACTIVE
```

### Planning path
@@ -163,11 +163,11 @@ sequenceDiagram
    Q->>C: createResource(headers, body)
    C->>S: resourceLogic(ru, placeholderId, simulation=true)
    Note over S: computes characteristics,<br/>suppresses side effects
    C->>CB: RESERVED + estimate.creation.*
    C->>CB: RESERVED + HELD + estimate.creation.*
    CB-->>C: acknowledged resource id
    Note over C: ~2 s later, background thread
    C->>CB: AVAILABLE + estimate.operation.*
    CB-->>OSOM: RESERVED → AVAILABLE observed,<br/>plan converges with costs attached
    Note over C: a moment later, background thread
    C->>CB: AVAILABLE + UP + estimate.operation.*
    CB-->>OSOM: HELD → UP observed,<br/>plan converges with costs attached
```

Three things are going on there, and each is a rule worth copying.
@@ -176,11 +176,16 @@ Three things are going on there, and each is a rule worth copying.
tentative plan reflects the characteristics the controller *actually* computes — not the placeholder
OSOM guessed. A dry-run that skips the logic tells the planner nothing useful.

**Model deployment as taking time.** The controller reports `RESERVED` synchronously, then `AVAILABLE`
on a scheduled callback `PLANNING_SIMULATED_DEPLOY_DELAY_MS` later. OSOM's planning check-deployment
loop watches for exactly that progression. Reporting `AVAILABLE` immediately can race the loop; never
reporting it means the plan hangs until the wait budget expires. Keep the delay comfortably below
OSOM's budget — 2 s by default.
**Model deployment as taking time.** The controller reports the resource **HELD** synchronously, then
**UP** on a scheduled callback `PLANNING_SIMULATED_DEPLOY_DELAY_MS` later (200 ms by default). OSOM's
planning check-deployment loop watches for exactly that progression. Reporting `UP` immediately can
race the loop; never reporting it means the plan hangs until the wait budget expires. Keep the delay
comfortably below OSOM's budget.

Both callbacks post the *same* `ResourceUpdate` as the body, and `resourceLogic` has already stamped
ENABLE/UNLOCKED on it. That is why the health attributes go out as explicit **headers**, one set per
phase: OSOM reads headers before the body, so without them the synchronous "deployment requested"
post would read UP straight away and the whole progression would collapse into a single state.

**Target the id OSOM acknowledged.** The first callback returns a resource; its UUID is what the second
callback must address, or you create a second `ExpectedResource` instead of updating the first:
@@ -189,6 +194,56 @@ callback must address, or you create a second `ExpectedResource` instead of upda
String effectiveId = (reported != null && reported.getUuid() != null) ? reported.getUuid() : resourceid;
```

### Health, not status

A resource carries two independent vocabularies, and only one of them moves a service.

`resourceStatus` is **pool bookkeeping**: `AVAILABLE` means "free in a pool", `RESERVED` means "held
for someone", `UNKNOWN` means nobody is tracking it any more. It says nothing about whether the thing
works, and OSOM's service-state fold does not read it.

The three ITU-T X.731 attributes are the state of the resource **itself**:

| Attribute | Values | What it answers |
|---|---|---|
| `operationalState` | `ENABLE` / `DISABLE` | does it work |
| `administrativeState` | `UNLOCKED` / `LOCKED` / `SHUTDOWN` | is it allowed to be used |
| `usageState` | `IDLE` / `ACTIVE` / `BUSY` | is it carrying load — informational only |

`Resource.deriveHealth()` folds the first two into a verdict, and the owning service's lifecycle state
follows that verdict. `usageState` takes no part in it: a healthy, deployed resource carrying no load
is legitimately `IDLE`.

| What you report | Health | What happens to the supporting service |
|---|---|---|
| `ENABLE` + `UNLOCKED` | `UP` | reaches ACTIVE |
| `DISABLE` + `UNLOCKED` | `DOWN` | demoted — the resource is broken |
| `ENABLE` + `LOCKED` | `HELD` | stays put — reserved, not yet in service |
| anything + `SHUTDOWN` | `GONE` | torn down, reaches TERMINATED |
| either attribute unset | `PENDING` | **nothing** — pinned where it is, indefinitely |

That last row is the trap, and it is why this section exists. A controller that sets only
`resourceStatus` leaves both attributes null, which reads as "this producer has not reported" —
deliberately, so a controller that has not adopted the contract can neither promote nor demote a
service by accident. The order does not fail; it simply never finishes, with nothing in any log to say
why. **Every path that reports a resource must declare health**, teardown included: `UNKNOWN` on its
own folds to `PENDING`, so a delete without `SHUTDOWN` leaves the service hanging instead of
terminating it.

On the production path you set the attributes on the `ResourceUpdate` you hand to the catalog. On the
planning path you send them as headers on the callback:

```
org.etsi.osl.operationalState      ENABLE | DISABLE
org.etsi.osl.administrativeState   UNLOCKED | LOCKED | SHUTDOWN
org.etsi.osl.usageState            IDLE | ACTIVE | BUSY
```

Both the enum name and the TMF wire value (`enable`, `unlocked`) are accepted. Rather than keeping a
second copy of the table above, ask the model to spell a verdict out for you —
`ResourceRepoService.putHealth` builds a throwaway `Resource`, calls `applyHealth(health)` and reads
the attributes back. A hand-copied mapping is exactly the thing that drifts.

---

## 4. The estimate contract
@@ -204,12 +259,12 @@ org.etsi.osl.estimate.<phase>.cost

where `<phase>` is `creation`, `operation` or `teardown`, mapped onto the callbacks as:

| Callback | Status | Phase |
|---|---|---|
| create, synchronous | `RESERVED` | `creation` |
| create, delayed | `AVAILABLE` | `operation` |
| update (planning) | `AVAILABLE` | `operation` |
| delete (planning) | `UNKNOWN` | `teardown` |
| Callback | Status | Health | Usage | Phase |
|---|---|---|---|---|
| create, synchronous | `RESERVED` | `HELD` (ENABLE/LOCKED) | `IDLE` | `creation` |
| create, delayed | `AVAILABLE` | `UP` (ENABLE/UNLOCKED) | `ACTIVE` | `operation` |
| update (planning) | `AVAILABLE` | `UP` (ENABLE/UNLOCKED) | `ACTIVE` | `operation` |
| delete (planning) | `UNKNOWN` | `GONE` (DISABLE/SHUTDOWN) | `IDLE` | `teardown` |

**Units are fixed by the contract and never transmitted:** seconds, Watt-hours, dimensionless cost
units. Do not invent a unit header; agree on these.
@@ -351,7 +406,13 @@ public class VirtualMachine extends ExampleResourceSpec {
                EValueType.TEXT.getValue());
        ru.addResourceCharacteristicItemShort("status.Health", "Healthy", EValueType.TEXT.getValue());

        ru.setResourceStatus(ResourceStatusType.AVAILABLE);   // ← or OSOM waits forever
        // Pool bookkeeping first, then the health that actually moves the service (see "Health, not
        // status" above). Without the two X.731 attributes health reads PENDING and the supporting
        // service never leaves RESERVED — no error, no log line, just an order that never finishes.
        ru.setResourceStatus(ResourceStatusType.AVAILABLE);
        ru.setOperationalState(ResourceOperationalStateType.ENABLE);
        ru.setAdministrativeState(ResourceAdministrativeStateType.UNLOCKED);
        ru.setUsageState(ResourceUsageStateType.ACTIVE);
        return ru;
    }
}
@@ -383,14 +444,15 @@ catalogue and can be referenced from a service specification.

The example logic is synchronous and instant. Real provisioning is neither. Two options:

- **Block briefly** in `resourceLogic` and return `AVAILABLE` — fine for sub-second work.
- **Return `RESERVED` immediately**, do the work on a background thread, then call
  `catalogClient.updateResourceById(resourceId, ru)` with `AVAILABLE` when it finishes. This is the right
  shape for anything slow, and it is exactly what the planning path already models.
- **Block briefly** in `resourceLogic` and return `ENABLE`/`UNLOCKED` — fine for sub-second work.
- **Return `ENABLE`/`LOCKED`** (health `HELD`) immediately, do the work on a background thread, then
  call `catalogClient.updateResourceById(resourceId, ru)` with `ENABLE`/`UNLOCKED` when it finishes.
  This is the right shape for anything slow, and it is exactly what the planning path already models.

Either way, **something must eventually set a terminal status**, or OSOM's wait loop never converges and
the service order stalls. On failure, report the resource with a status that reflects it and put the
reason in `status.infoMessage` — a failed resource is far more useful to an operator than a hung one.
Either way, **something must eventually report a terminal health**, or OSOM's wait loop never converges
and the service order stalls. On failure report `DISABLE` + `UNLOCKED` — health `DOWN`, which demotes
the service instead of leaving it pinned — and put the reason in `status.infoMessage`. A failed
resource is far more useful to an operator than a hung one, and `PENDING` is what "hung" looks like.

---

@@ -435,6 +497,8 @@ a test pinning the boundary with the string `"polygenelubricants"`.
| `org.etsi.osl.operationDurationSeconds` | Planning only: the window to size the operation estimate against |
| `X-OSL-SIMULATION` | Present ⇒ dry-run. Suppress all side effects. |

The health headers you send back on a planning callback are listed under "Health, not status" in §3.

---

## 7. Testing
@@ -462,7 +526,13 @@ void producesACompleteRegistrationPayload(ExampleResourceSpec spec) { ... }
```

The registration payload is sent over JMS and its reply is discarded, so a malformed one fails silently
in production. That is exactly the kind of thing to assert offline.
in production. That is exactly the kind of thing to assert offline. The same harness asserts that every
spec reports the full X.731 triple, so a spec that forgets one fails here rather than by hanging an
order in a running platform.

**Assert what goes on the wire, per path.** `ResourceRepoServiceHealthTest` captures the headers on a
mocked `ProducerTemplate` and pins each callback's health: create posts `HELD` then `UP`, delete posts
`GONE`. These are one-line-per-path facts that are invisible without a broker and an orchestrator.

**Pin the contract, not the figures.** The estimate numbers are invented and will change. What the tests
lock in is the *shape*: that the window scales the operation phase, that a duty cycle scales cost but not
@@ -474,6 +544,7 @@ duration, that clamping holds, and that measured-zero and unreported stay distin

| Doc | What it covers |
|---|---|
| [Health, not status](20260827_1305_x731-health-on-every-path.md) | The ITU-T X.731 triple on every path, and why reporting `resourceStatus` alone hangs an order forever |
| [Duty-cycled operation estimates](20260718_2236_duty-cycle-engineer-operation-estimate.md) | Billing hours worked vs hours elapsed; why the estimate reads the request |
| [More example resource specs](20260716_1615_more-example-resource-specs.md) | Growing the catalogue to 10 specs; the base class and registry refactor |
| [Report lifecycle estimates](20260716_1430_report-lifecycle-estimates.md) | The estimate header contract and the `/PLANNING` routes |
+179 −0
Changes for docs/20260827_1305_x731-health-on-every-path.md: 179 added lines, 0 removed lines.
Original line number Diff line number Diff line
# Health, not status — the X.731 triple on every path

**Date:** 2026-08-27

## What was broken

OSOM used to decide a supporting service's lifecycle state from the resource's `resourceStatus`. It
no longer does. `docs/20260827_0217_service-state-from-resource-health.md` in OSOM moved the verdict
onto **resource health** — `Resource.deriveHealth()`, which folds the two ITU-T X.731 attributes
`operationalState` and `administrativeState` into `UP` / `DOWN` / `HELD` / `GONE` / `PENDING`.
`resourceStatus` went back to being what it always meant: pool bookkeeping.

Producers in `model.k8s`, `cridge`, `metrico` and OSOM itself were converted at the time. **This
project was not**, and it was a pure `resourceStatus` producer: all ten example specs ended
`resourceLogic` with the single line `resourceUpdate.setResourceStatus(ResourceStatusType.AVAILABLE)`
and set no X.731 attribute anywhere.

The consequence was not a degraded plan or a wrong figure. It was a hang:

- **Ordering any example service stopped working.** The resource reached the catalog with
  `operationalState=null, administrativeState=null`, which derives to `PENDING` — "this producer has
  not reported" — and a `PENDING` resource neither promotes nor demotes its service. The service sat
  at `RESERVED` indefinitely. No exception, no failed order, nothing in any log to point at.
- **Terminating one did not work either.** `ResourceRepoService.deleteResource` wrote
  `resourceStatus=UNKNOWN`, which also derives to `PENDING`, so the service was never torn down.
- **The planning path only worked by accident**, through the status-derived compatibility table in
  OSOM's `PlanningResourceCallbackHandler.resolveHealth` — the fallback that exists for controllers
  which have not been upgraded yet, and precisely the thing the reference example should be
  demonstrating how *not* to need.

That last point is why this project mattered more than its size suggests: it is the document and the
codebase every controller author is pointed at, and its authoring guide still taught the old contract
in a code comment reading `// ← or OSOM waits forever`.

## What changed

### The ten specs declare the triple themselves

`ResourceA`, `ResourceB`, `VirtualMachine`, `VpnAccess`, `KubernetesCluster`, `Core5G`, `RadioNode`,
`Cpe`, `MobilePhone`, `RadioEngineer` each now end `resourceLogic` with the same four lines:

```java
// Deployed and in service: AVAILABLE + ENABLE/UNLOCKED/ACTIVE.
resourceUpdate.setResourceStatus(ResourceStatusType.AVAILABLE);
resourceUpdate.setOperationalState(ResourceOperationalStateType.ENABLE);
resourceUpdate.setAdministrativeState(ResourceAdministrativeStateType.UNLOCKED);
resourceUpdate.setUsageState(ResourceUsageStateType.ACTIVE);
```

Written out in every spec rather than hoisted into a base-class helper. This is a teaching project
and a spec class is copied as a template, so the contract has to be visible in the file an author
copies; a helper would hide the single most important four lines behind a method name.
`ResourceA` and `ResourceB` — the two an author reads first — carry the full explanation as a
comment instead of the one-liner.

`usageState=ACTIVE` because these resources are dedicated to the service that ordered them, so once
deployed they are in use. It plays no part in the health verdict and never moves a service state; it
is reported because the operator now sees it in the plan view, and because a reference example
should show all three attributes rather than the two that happen to be load-bearing.

### `ResourceRepoService` reports health on every path

The planning callbacks now carry explicit health headers, per phase:

| Callback | `resourceStatus` | Health | Usage | Phase |
|---|---|---|---|---|
| create, synchronous | `RESERVED` | `HELD` (ENABLE/LOCKED) | `IDLE` | `creation` |
| create, delayed | `AVAILABLE` | `UP` (ENABLE/UNLOCKED) | `ACTIVE` | `operation` |
| update (planning) | `AVAILABLE` | `UP` (ENABLE/UNLOCKED) | `ACTIVE` | `operation` |
| delete (planning) | `UNKNOWN` | `GONE` (DISABLE/SHUTDOWN) | `IDLE` | `teardown` |

Headers here are mandatory, not decorative. OSOM resolves health header-first, body-second, and
`postPlanningResourceCallback` serialises the **same** `ResourceUpdate` object as the body for both
create callbacks — which, after the change above, says ENABLE/UNLOCKED. Without an explicit header
the synchronous "deployment requested" post would read `UP` immediately and the HELD → UP progression
this controller exists to demonstrate would collapse into a single state.

The attribute values are derived by calling `Resource.applyHealth(health)` on a throwaway `Resource`
and reading them back, rather than re-tabulating the mapping. `applyHealth` is the model's single
source of truth for how a verdict is spelled out, and a hand-written second copy is exactly what
drifts — `cridge`'s `PlanningCallbackClient` carries one, and it already disagrees with OSOM's
fallback about `RESERVED`.

Three smaller fixes on the same pass:

- **Delete, real path** — writes `DISABLE` / `SHUTDOWN` / `IDLE` next to `resourceStatus=UNKNOWN`.
  `SHUTDOWN` is the teardown tombstone the new fold reads. `resourceStatus` stays `UNKNOWN` because
  health now carries the teardown meaning and these bespoke resources return to no pool.
- **`toSimulatedResource`** — the local echo returned when the callback queue is unreachable now says
  what the callback would have said (`applyHealth(UP)` + `ACTIVE`), instead of a bare `AVAILABLE`.
- **A dead store in the delete simulation branch** — the method built a `Resource`, set
  `resourceStatus=UNKNOWN` on it, then threw it away and returned a *second*, freshly built one that
  said `AVAILABLE`. So the delete echo reported the opposite of what had happened. Fixed at source.

### OSOM could not carry `usageState` at all

`PlanningResourceCallbackHandler` accepted `operationalState` and `administrativeState` and nothing
else, so a controller had no way to report the third attribute even though OSOM's API and UI display
it — every planning resource showed the model's `IDLE` default regardless. It now accepts an
`org.etsi.osl.usageState` header (enum name or TMF wire value), falls back to the posted body, and
drops an unrecognised value with a warning rather than propagating it or clearing the default. It
takes no part in the status-derived compatibility table: that table exists to infer *health*, and an
inventory status implies nothing about utilisation. Documented in OSOM's own
`docs/20260827_1310_planning-callback-usage-state.md`.

### The documentation taught the old contract

Both the README and the authoring guide told an author to set `resourceStatus` and stop — the guide's
walkthrough sample carried the comment `// ← or OSOM waits forever` on exactly the line that no longer
ends the wait loop. Following either document verbatim now produced a controller that hangs. They are
the deliverable here as much as the code is:

- A new **"Health, not status"** section in the guide states the contract once: the two vocabularies,
  the five verdicts and what each does to the owning service, the `PENDING` trap, and the three header
  names. Everything else that touched the subject now points at it instead of restating it.
- Both sequence diagrams, the estimate-contract callback table (which gains health and usage columns),
  the walkthrough sample, and Step 5's failure advice (on failure, report `DISABLE`+`UNLOCKED`, health
  `DOWN`, which demotes the service rather than pinning it) were corrected to match.
- The README's spec sample and its "you must set a terminal status" paragraph likewise.

## Two pre-existing defects fixed in passing

The test suite in this project was already red before any of the above — **20 failures out of 67** at
`HEAD`. Both causes were unrelated to X.731, and both were fixed at source rather than worked around:

- **14 failures: `status.*` characteristics were never registered.**
  `ExampleResourceSpec.toRSpecCreate_InitRepo()` registered only `Kind`, although its own javadoc —
  and the authoring guide's comment `// the shared Kind/status.* ones are added for you` — promised
  the two `status.infoMessage` / `status.Health` characteristics every `resourceLogic` reports back.
  A characteristic the specification never declared is not part of the spec's contract, so nothing
  downstream knows to display it. They are now registered through a new `addReportedCharacteristic`
  helper: no default value to prefill and not configurable, because an operator does not *request* a
  status, the controller *reports* one.
- **6 failures: an invented labour rate drifted from its tests.** Commit `1ef30cc` raised
  `RadioEngineer.profileFor`'s hourly rate from 45.0 to 50.0 without touching the two test classes
  asserting against it. The code is authoritative, so the tests were corrected and collapsed onto a
  single shared `RadioEngineerTest.RATE` constant, which is what stops the same drift recurring.

## Tests

- `ExampleResourceSpecTest` — the existing parameterized harness (it already feeds every spec from
  `ResourceSpecCatalogConfig`, so a spec added later is covered automatically) gains
  `reportsItselfInServiceWithTheFullX731Triple` and a second test that folds what the spec reports
  through `deriveHealth()` and asserts `UP`. That second one states the assertion the way OSOM
  actually reads it, and is the test that would have caught the original gap.
- `ResourceRepoServiceHealthTest` (new) — captures the headers on a mocked `ProducerTemplate` and
  pins each path: create posts `HELD` then `UP`, planning update posts `UP`, planning delete posts
  `GONE`; the real delete's `ResourceUpdate` reaching `CatalogClient` carries the same tombstone and
  folds to `GONE`; and both queue-unreachable echoes report what their callback would have. The
  spec-holding fields on `ResourceRepoService` are package-private, so a same-package test injects a
  mock directly and `ReflectionTestUtils` supplies the two `@Value` fields.
- OSOM's `PlanningResourceCallbackHandlerTest` gains five `usageState` cases: header by enum name and
  by TMF value, body fallback, an unrecognised value leaving the default alone, and a status-only
  callback that still derives health while leaving usage untouched.

`mvn clean test`: **93/93 green** here (from 67 run / 20 failures at `HEAD`), **1216/1216** in OSOM
(from 1211).

## Verifying against a running platform

The tests cover what goes on the wire; they cannot cover what OSOM does with it. With an OpenSlice
instance and `mvn spring-boot:run`:

1. Order an example service (e.g. `VirtualMachine`). The catalog resource should carry
   `operationalState=enable`, `administrativeState=unlocked`, `usageState=active` beside
   `resourceStatus=available`, and the service should reach **ACTIVE** — before this change it
   stopped at `RESERVED`.
2. Terminate it. The resource goes `DISABLE` / `SHUTDOWN` and the service reaches **TERMINATED**.
3. Re-plan the same order (`POST /api/tentative-planning/{orderId}/replan`). `/manage/view/{orderId}`
   should show the resource progressing `RESERVED · HELD` → `AVAILABLE · UP` with the
   `op ENABLE · admin UNLOCKED · usage ACTIVE` sub-line. The `usage` part is the OSOM fix — before
   it, that line always read `usage IDLE`.

## Not done

`addons/org.etsi.osl.controllers.capif.invoker` has exactly the same defect: it writes
`resourceStatus` and no X.731 attribute, so any service it supports will hang the same way. `giter`
and `ietf.ns` already touch X.731 and need a closer look rather than a blanket change. Out of scope
here — the ask was this project — but worth a follow-up pass.
+81 −0

File added.

Preview size limit exceeded, changes collapsed.

+2 −0

File changed.

Preview size limit exceeded, changes collapsed.

Loading