Isolation as an operator policy A one-for-one contract and a siblings-touched query
Refuse a policy that would restart a sibling or leave the cap unbounded, then ask of every apply and report whether any sibling was touched and whether the ensemble is healthy after a worker gave up.
Lab download. Starter is the tree you edit. Worked answer is a separate file; open it after the starter passes. Nickel 1.18 and Nushell 0.116. The reference tree's jq-report.sh also needs jq 1.7. All lab requirements.
O-SWE · A · arc: self-healing-edge · langs: nickel, nushell · CKA-WORKLOADS-self-healing
By Evan Eugene Pincham.
Concept
A supervisor is a process whose only job is to start other processes and to decide what happens when one of them exits. The processes it starts are its workers. Keeping that decision in one place means the workers carry no restart logic, and an operator can read, review and change the rule without touching worker code.
A restart strategy is the rule that maps one worker's exit onto the set of workers that get restarted. The Erlang/OTP system documentation, Supervisor Behaviour, describes the common strategies. Under one_for_one, the default, "If a child process terminates, only that process is restarted." Under one_for_all, every child is terminated and restarted when any one of them fails. The same page also describes rest_for_one, which restarts the failed child and the children started after it. The choice matters because a strategy encodes a claim about dependency: one_for_one says the workers are independent, and one_for_all says they are not.
Joe Armstrong's thesis, Making reliable distributed systems in the presence of software errors (KTH, 2003), draws the same line in section 5.2.3, "And/or supervision hierarchies". An OR supervisor restarts the child that died. Activities in an OR tree are assumed independent, so the failure of one child has no consequence for the other children. Chapter 6 builds such a supervisor with {one_for_one,5,1000} because the three supervised activities are considered unrelated. It adds that more than 5 restarts in 1000 seconds makes the supervisor itself fail. That limit is the cap: it turns an endless crash loop into a failure that somebody can see.
The claim of independence can be wrong in either direction, which is why an operator should state it explicitly. If two independent workers run under one_for_all, a single fault becomes two outages. If two dependent workers run under one_for_one, the survivor keeps serving against a partner that is gone. This lesson fixes the first case: the pair is api and scorer, and they are declared independent. The name scorer stands in for a sidecar, a helper process that runs next to the main process and serves it while keeping its own life cycle. No model runs and nothing is trained. The name only marks a worker whose crash must not restart the request path.
Kubernetes documents the same split at the container level. The Pod Lifecycle page says "If one of the containers in the Pod fails, then Kubernetes may try to restart that specific container." It says sidecar containers "can restart independently of the main application container". It also describes RestartAllContainers, an explicit opt-in that terminates and restarts the entire Pod in place, which is the cluster analog of one_for_all. The ReplicaSet page says "A ReplicaSet's purpose is to maintain a stable set of replica Pods running at any given time." It compares the controller to a process supervisor that watches many Pods across many nodes, and it states that a ReplicaSet delegates local container restarts to an agent on the node such as the Kubelet. These pages are read as prose only. No cluster is run here.
The Certified Kubernetes Administrator (CKA) curriculum v1.37 lists, under "Workloads and Scheduling", the bullet "Understand the primitives used to create robust, self-healing, application deployments". One-for-one isolation is the local stand-in for restarting the container, or replacing the Pod, that failed without taking an unrelated worker with it. ReplicaSet and Deployment are the cluster primitives that the mapping names.
The companion Rust lesson, "A one-for-one supervisor that restarts only the worker that failed, in Rust", builds a tool called isolate. Two of its commands matter here. isolate apply PLAN reads a plan, which is a JSONL file with one JSON object per line describing a worker event such as started, heartbeat or exited. It prints one JSON action per decision, and that stream is the apply output. isolate report PLAN applies the same rule and prints key: value lines, and that text is the report output. The default policy is strategy one_for_one, max_restarts = 3 and workers api and scorer.
Each action carries an event, a worker, an attempt number, the cap as max, and a field named siblings_touched. The field lists any other worker that the supervisor restarted because of this exit. Under one_for_one it is always the empty list, and no action line appears for a sibling. When a worker exits more times than the cap allows, the action event becomes gave_up for that worker alone. The cap exists because an unbounded restart loop hides a permanent fault and consumes resources while it does so.
The report adds one more idea. The ensemble is the pair of workers read as a single service, and its state is healthy, recovering or degraded. It is healthy only when every worker is healthy, and it is degraded whenever any worker has gave_up. The rule exists because a ledger that calls the pair healthy after one worker gave up hides the single fact that an operator most needs to see.
This lesson takes the operator's side of that tool. An isolation policy is the record that names the strategy, the cap and the workers. A Nickel contract is a check attached to a value in the Nickel configuration language, and evaluation stops with an error when the value breaks it. The Nickel manual, "Contracts", says record contracts "encode the data schema of a configuration". The contract here refuses a policy that would restart a sibling or leave the cap unbounded, before any supervisor binds that policy. Nushell is a shell whose pipelines carry structured data, and it is used here to query committed apply and report output. A violation is a named finding that an output contradicts the isolation rule. The query exists because the supervisor's own output is only a claim, and an operator wants an independent reader that can name where the claim and the rule disagree.
A bounded restart of one process is not enough once two workers share a supervisor. If they are independent, a crash in one must not restart the other, and the ledger must not call the pair healthy after one of them gave up. A hand-edited apply output, kept here as forged.apply, shows what a broken ledger looks like. It is named and counted, and it is never trusted.
Honesty line: this is a decision kernel over committed text fixtures, meaning a small program that applies the isolation rule to files and writes what a supervisor would decide, without starting the workers. It runs on a CPU, starts no live child processes and uses no cluster. The isolation rule, the per-worker cap, the ensemble rule and the action ledger are real. Spawning workers, an OTP runtime and any Kubernetes API are not run.
Worked example
The fixtures come from the companion lesson. The first one is the apply output for an api that exits four times while scorer only starts.
{"event":"restarting","worker":"api","attempt":1,"max":3,"siblings_touched":[]}
{"event":"restarting","worker":"api","attempt":2,"max":3,"siblings_touched":[]}
{"event":"restarting","worker":"api","attempt":3,"max":3,"siblings_touched":[]}
{"event":"gave_up","worker":"api","attempt":3,"max":3,"siblings_touched":[]}
Three restarts are allowed, so the fourth exit produces gave_up with attempt 3 of max 3. Every siblings_touched list is empty, and no line names scorer. This is the honest shape under one_for_one. The next fixture is hand-edited and breaks the rule in two places.
{"event":"restarting","worker":"api","attempt":1,"max":3,"siblings_touched":["scorer"]}
{"event":"restarting","worker":"scorer","attempt":1,"max":3,"siblings_touched":[]}
The first line claims that an api restart touched scorer, and the second line shows a restarting action for scorer. That pairing is what one_for_all would produce, so a reader who inspects only the first line sees one problem and a reader who inspects both sees two. The query in this lesson reports both.
The contract comes first, because it decides whether a policy may exist at all.
# isolate-policy.ncl: the contract every isolation policy must satisfy.
# `nickel export` refuses a policy that would restart a sibling or leave the cap unbounded.
let Strategy = std.contract.from_validator (fun s =>
if s == "one_for_one" then 'Ok
else 'Error { message = "strategy must be one_for_one" })
in
let Bounded = fun lo hi label =>
std.contract.from_validator (fun n =>
if std.is_number n && std.number.is_integer n && n >= lo && n <= hi then 'Ok
else 'Error { message = "%{label} must be an integer from %{std.to_string lo} to %{std.to_string hi}" })
in
let IndependentWorkers = std.contract.from_validator (fun xs =>
if std.is_array xs
&& std.array.length xs == 2
&& std.array.elem "api" xs
&& std.array.elem "scorer" xs then 'Ok
else 'Error { message = "workers must be the independent pair api and scorer" })
in
{
IsolationPolicy = {
strategy | Strategy,
max_restarts | Bounded 1 10 "max_restarts",
workers | IndependentWorkers,
},
}
Each let defines one reusable contract. The Nickel manual, "User-defined contracts", explains that std.contract.from_validator wraps a function that returns 'Ok or 'Error { message = ... }, and the message becomes the refusal text. Strategy accepts exactly the string one_for_one. That choice also refuses one_for_all and rest_for_one, because the policy states independence and nothing weaker should pass.
Bounded is a function that takes a low bound, a high bound and a label, and it returns a validator. The test checks std.is_number first, so a string never reaches the integer check. It then checks std.number.is_integer and the range. The range 1 to 10 requires at least one restart and refuses anything that behaves like an unbounded cap. This kernel keeps only a count. The period of Armstrong's {one_for_one,5,1000} has no counterpart in the plan files.
IndependentWorkers requires an array of length two that contains both api and scorer. A third worker, or a different name in place of scorer, fails. The final record, IsolationPolicy, attaches one contract to each field with the | operator. It has no trailing ... The Nickel manual says a trailing .. makes a record contract open, so this contract stays closed.
A second file applies the contract to today's values.
# policy.ncl: today's isolation policy for the independent pair api and scorer.
let { IsolationPolicy, .. } = import "isolate-policy.ncl" in
{
strategy = "one_for_one",
max_restarts = 3,
workers = ["api", "scorer"],
} | IsolationPolicy
The import pulls in the record from the contract file, and the pattern { IsolationPolicy, .. } takes the one field it needs and ignores the rest. The plain record below it holds the operator's values. The trailing | IsolationPolicy attaches the contract to the whole record. Running nickel export policy.ncl evaluates the record, and any value that breaks a validator stops the export with that validator's message. A supervisor that reads only exported JSON never sees a refused policy. The checker runs the same contract against three probe policies and prints the outcomes.
policy accepted: {"max_restarts":3,"strategy":"one_for_one","workers":["api","scorer"]}
RESULT policy_accepted PASS
policy one_for_all: refused: strategy must be one_for_one
RESULT rejects_one_for_all PASS
policy unbounded_cap: refused: max_restarts must be an integer from 1 to 10
RESULT rejects_unbounded_cap PASS
policy unknown_worker: refused: workers must be the independent pair api and scorer
RESULT rejects_unknown_worker PASS
The first line shows the default policy exported as compact JSON. The next three show one probe each: a strategy of one_for_all, a max_restarts of 50, and a workers list where other replaces scorer. Each refusal text matches a validator message in the contract word for word. The message wording was observed in Nickel 1.18.0 and may differ in other releases.
The Nushell script reads either kind of output. It decides which kind it has by looking at the first non-blank character.
# isolate-report.nu: were any siblings touched? is the ensemble healthy after a worker gave up?
# Reads one `isolate apply` JSONL file or one `isolate report` key: value file.
# Usage: nu isolate-report.nu <apply-or-report>
# Exit 0 when an apply touched no siblings (and names no violations) or a report
# may honestly call the ensemble healthy, 1 when siblings were touched, the
# ensemble is not healthy, or a field contradicts the isolation rule, 2 when
# the file cannot be opened.
def pick [rows: table, name: string] {
let hit = ($rows | where key == $name)
if ($hit | is-empty) { "missing" } else { $hit | get value | first }
}
def main [input: path] {
if not ($input | path exists) {
print -e $"isolate-report.nu: cannot open ($input)"
exit 2
}
let raw = (open --raw $input)
let nonempty = ($raw | lines | each {|l| $l | str trim } | where {|l| $l != ""})
if ($nonempty | is-empty) {
print -e $"isolate-report.nu: cannot open ($input)"
exit 2
}
let first = ($nonempty | first)
if ($first | str starts-with "{") {
let actions = ($nonempty | each {|l| $l | from json })
let touched = ($actions | each {|a| $a.siblings_touched? | default [] } | flatten | uniq)
let workers = ($actions | each {|a| $a.worker } | uniq)
let gave = ($actions | where event == "gave_up" | each {|a| $a.worker })
mut v = []
for a in $actions {
let sibs = ($a.siblings_touched? | default [])
if ($sibs | is-not-empty) {
let names = ($sibs | str join ", ")
$v = ($v | append $"siblings_touched lists ($names) on ($a.worker) ($a.event)")
}
}
for w in $touched {
$v = ($v | append $"sibling action emitted for ($w)")
}
let siblings_any = ($touched | is-not-empty)
print $"isolate-report: ($input | path basename)"
print "kind: apply"
print $"actions: ($actions | length)"
print $"workers: ($workers | str join ', ')"
print $"siblings_touched: (if $siblings_any { 'yes' } else { 'no' })"
print $"sibling_names: ($touched | str join ', ')"
print $"sibling_actions: ($actions | where {|a| $a.worker in $touched } | length)"
print $"gave_up: (if ($gave | is-empty) { 'none' } else { $gave | str join ', ' })"
print $"violations: ($v | length)"
for line in $v { print $" ($line)" }
if $siblings_any or ($v | is-not-empty) { exit 1 }
} else {
let rows = ($raw | lines | parse -r '^(?<key>[a-z_.]+): (?<value>.*)$')
let strategy = (pick $rows "strategy")
let max_restarts = (pick $rows "max_restarts")
let api_state = (pick $rows "worker.api.state")
let api_restarts = (pick $rows "worker.api.restarts")
let scorer_state = (pick $rows "worker.scorer.state")
let scorer_restarts = (pick $rows "worker.scorer.restarts")
let siblings_restarted = (pick $rows "siblings_restarted")
let ensemble = (pick $rows "ensemble")
let any_gave_up = ($api_state == "gave_up") or ($scorer_state == "gave_up")
let both_healthy = ($api_state == "healthy") and ($scorer_state == "healthy")
mut v = []
if ($ensemble == "healthy") and $any_gave_up {
let who = (if $api_state == "gave_up" and $scorer_state == "gave_up" { "api and scorer"
} else if $api_state == "gave_up" { "api"
} else { "scorer" })
$v = ($v | append $"ensemble healthy after ($who) gave_up")
}
let ensemble_healthy = ($ensemble == "healthy") and $both_healthy and (not $any_gave_up) and ($v | is-empty)
let reason = (if ($v | is-not-empty) { "report disagrees with isolation rule"
} else if $ensemble_healthy { "ensemble healthy, no worker gave up"
} else { $"ensemble ($ensemble)" })
print $"isolate-report: ($input | path basename)"
print "kind: report"
print $"strategy: ($strategy)"
print $"max_restarts: ($max_restarts)"
print $"worker.api.state: ($api_state)"
print $"worker.api.restarts: ($api_restarts)"
print $"worker.scorer.state: ($scorer_state)"
print $"worker.scorer.restarts: ($scorer_restarts)"
print $"siblings_restarted: ($siblings_restarted)"
print $"ensemble: ($ensemble)"
print $"ensemble_healthy: (if $ensemble_healthy { 'yes' } else { 'no' })"
print $"violations: ($v | length)"
for line in $v { print $" ($line)" }
print $"reason: ($reason)"
if not $ensemble_healthy { exit 1 }
}
}
The helper pick looks up one key in a table of parsed rows and returns the string missing when the key is absent. A report with a lost line therefore cannot pass as healthy, because missing never equals healthy. In main, the script exits with status 2 when the path does not exist or when the file holds only blank lines. That status is separate from 1 so that a caller can tell "the check found a problem" from "the check could not run". The Nushell reference for open says --raw opens a file as raw text, which keeps the lines intact for both branches.
The apply branch parses each non-blank line with from json, a command the Nushell reference describes as turning JSON text into structured data. It then builds touched, the flattened and de-duplicated union of every siblings_touched list. The ? after the field name and the default [] mean that a line without the field counts as an empty list. The branch records two kinds of violation. One is a line whose list is non-empty, and the other is a sibling action, where every name in touched yields a finding. On forged.apply, touched is ["scorer"], so the api line produces the first finding and the scorer name produces the second, which makes two. The script exits 1 if either list is non-empty and 0 otherwise. A gave_up event alone is not a violation, because one worker giving up is the honest outcome of its own cap.
The report branch uses parse -r, which the Nushell reference describes as parsing columns from a string with a full regular expression and named capture groups. Each line becomes a row with a key and a value. The script then derives two booleans from the worker states. A violation appears when the ensemble line says healthy while either worker is gave_up. The final flag, ensemble_healthy, is true only when the ensemble line says healthy, both workers are healthy, nobody gave up and no violation was found. Exit 1 means "may not honestly be called healthy". It does not mean "the report lied", and the violations count carries that second meaning. The outputs below show the difference.
isolate-report: gaveup.apply
kind: apply
actions: 4
workers: api
siblings_touched: no
sibling_names:
sibling_actions: 0
gave_up: api
violations: 0
For gaveup.apply, only api appears among the workers and gave_up names it. No sibling was touched, there are no violations, and the exit status is 0. The sibling_names line is empty because the list is empty.
isolate-report: gaveup.report
kind: report
strategy: one_for_one
max_restarts: 3
worker.api.state: gave_up
worker.api.restarts: 3
worker.scorer.state: healthy
worker.scorer.restarts: 0
siblings_restarted: 0
ensemble: degraded
ensemble_healthy: no
violations: 0
reason: ensemble degraded
The matching report shows api at the cap with three restarts and scorer untouched with zero. The ensemble is degraded, so ensemble_healthy is no and the exit status is 1. Because the report is consistent with the rule, violations stays at 0. The reason line gives the short cause.
isolate-report: forged.apply
kind: apply
actions: 2
workers: api, scorer
siblings_touched: yes
sibling_names: scorer
sibling_actions: 1
gave_up: none
violations: 2
siblings_touched lists scorer on api restarting
sibling action emitted for scorer
The forged file now produces two named findings, one for each place the one_for_all lie shows up, and the exit status is 1. The checker summarizes the three exit statuses in single lines.
gaveup.apply: exit=0 siblings_touched=no gave_up=api
gaveup.report: exit=1 ensemble_healthy=no ensemble=degraded api=gave_up
forged: exit=1 siblings_touched=yes violations=2
Reading these lines side by side explains the exit convention. An api that gave up is honest on the apply side and not healthy on the report side. A forged ledger is a finding on its own.
Three pitfalls are worth knowing, each as a symptom and a cause. A report that prints ensemble: degraded next to ensemble_healthy: yes is a symptom of a script that hardcodes its verdict and never combines the worker states with the ensemble line. A forged.apply that exits 0 with violations: 0 is a symptom of code that never reads siblings_touched from the action lines. A gaveup.report that exits 1 while violations reads 0 can look like a contradiction, and the cause is that the exit status answers whether the ensemble is healthy while the count answers whether the report disagrees with the rule.
Lab
The lab directory holds a checker, the six fixtures and two starter files. The starter policy is a contract that checks only shape.
# isolate-policy.ncl (starter): the operator's isolation rules, as a contract.
# TODO: strategy must be one_for_one (refuse one_for_all).
# TODO: max_restarts must be an integer from 1 to 10 (a real per-worker cap).
# TODO: workers must be the independent pair api and scorer.
{
IsolationPolicy = {
strategy | String,
max_restarts | Number,
workers | Array String,
..
},
}
The annotations String, Number and Array String accept any value of that type, so one_for_all, a cap of 50 and a workers list with other all pass. The trailing .. makes the record open, so extra fields pass too. The default policy is accepted, and all three probe policies are accepted as well, which is the bug.
The starter Nushell file opens the input and tells apply files from report files. After that it prints constants. This is its apply branch.
if ($first | str starts-with "{") {
print "kind: apply"
print "siblings_touched: no"
print "sibling_actions: 0"
print "gave_up: none"
print "violations: 0"
These five prints are the whole answer for any JSONL input, whatever the content. The report branch prints the ensemble line it parsed but always follows it with ensemble_healthy: yes and violations: 0. The starter never exits 1, and its only non-zero exit is 2 for a missing path.
The task is to replace both starters with working versions. isolate-policy.ncl must refuse a strategy other than one_for_one, a max_restarts that is not an integer from 1 to 10, and a workers list that is not the independent pair api and scorer, while still accepting the default policy in policy.ncl. isolate-report.nu must name a non-empty siblings_touched list and any sibling action line, must refuse to call the ensemble healthy after any worker gave up, must exit 1 in those cases, and must exit 2 when the file cannot be opened. Exit 0 is for an apply that touched no siblings or a report that may honestly call the ensemble healthy.
Run the checker beside the finished files with sh check.sh. It needs Nickel 1.18 and Nushell 0.116 and uses no network. It runs 11 checks. Four exercise the contract: policy_accepted, rejects_one_for_all, rejects_unbounded_cap and rejects_unknown_worker. Seven exercise the report script: one_crash_siblings_untouched, gaveup_apply_honest, gaveup_ensemble_not_healthy, scorer_crash_siblings_untouched, healthy_ensemble, forged_siblings_named and missing_file_exit_2.
| Check | What it asserts |
|---|---|
| policy_accepted | The default policy exports with strategy, cap and both workers |
| rejects_one_for_all | A one_for_all policy is refused with the strategy message |
| rejects_unbounded_cap | A cap of 50 is refused with the range message |
| rejects_unknown_worker | A workers list with other in place of scorer is refused |
| one_crash_siblings_untouched | An api crash exits 0, touches no sibling and names only api |
| gaveup_apply_honest | Four api exits exit 0, touch no sibling and name api as gave_up |
| gaveup_ensemble_not_healthy | The report after api gave_up exits 1 and is not healthy |
| scorer_crash_siblings_untouched | A scorer crash exits 0, touches no sibling and names only scorer |
| healthy_ensemble | The all-started report exits 0 and is healthy |
| forged_siblings_named | The forged apply exits 1, reports siblings touched and counts 2 violations |
| missing_file_exit_2 | A path that does not exist exits 2 |
A finished lab prints this.
policy accepted: {"max_restarts":3,"strategy":"one_for_one","workers":["api","scorer"]}
RESULT policy_accepted PASS
policy one_for_all: refused: strategy must be one_for_one
RESULT rejects_one_for_all PASS
policy unbounded_cap: refused: max_restarts must be an integer from 1 to 10
RESULT rejects_unbounded_cap PASS
policy unknown_worker: refused: workers must be the independent pair api and scorer
RESULT rejects_unknown_worker PASS
one_crash: exit=0 siblings_touched=no workers=api
RESULT one_crash_siblings_untouched PASS
gaveup.apply: exit=0 siblings_touched=no gave_up=api
RESULT gaveup_apply_honest PASS
gaveup.report: exit=1 ensemble_healthy=no ensemble=degraded api=gave_up
RESULT gaveup_ensemble_not_healthy PASS
scorer_crash: exit=0 siblings_touched=no workers=scorer
RESULT scorer_crash_siblings_untouched PASS
healthy: exit=0 ensemble_healthy=yes ensemble=healthy
RESULT healthy_ensemble PASS
forged: exit=1 siblings_touched=yes violations=2
RESULT forged_siblings_named PASS
missing file: exit=2
RESULT missing_file_exit_2 PASS
A finished lab prints PASS on all eleven checks. The starter does not get there. It passes five checks and fails six, and the real starter output shows the split.
policy accepted: {"max_restarts":3,"strategy":"one_for_one","workers":["api","scorer"]}
RESULT policy_accepted PASS
policy one_for_all: accepted
RESULT rejects_one_for_all FAIL
policy unbounded_cap: accepted
RESULT rejects_unbounded_cap FAIL
policy unknown_worker: accepted
RESULT rejects_unknown_worker FAIL
one_crash: exit=0 siblings_touched=no workers=
RESULT one_crash_siblings_untouched PASS
gaveup.apply: exit=0 siblings_touched=no gave_up=none
RESULT gaveup_apply_honest FAIL
gaveup.report: exit=0 ensemble_healthy=yes ensemble=degraded api=
RESULT gaveup_ensemble_not_healthy FAIL
scorer_crash: exit=0 siblings_touched=no workers=
RESULT scorer_crash_siblings_untouched PASS
healthy: exit=0 ensemble_healthy=yes ensemble=healthy
RESULT healthy_ensemble PASS
forged: exit=0 siblings_touched=no violations=0
RESULT forged_siblings_named FAIL
missing file: exit=2
RESULT missing_file_exit_2 PASS
The passes are policy_accepted, one_crash_siblings_untouched, scorer_crash_siblings_untouched, healthy_ensemble and missing_file_exit_2. The fails are the three rejects_ checks, gaveup_apply_honest, gaveup_ensemble_not_healthy and forged_siblings_named. Two of the passes are accidental. A starter that prints constants happens to agree with the honest cases, so the checks one_crash_siblings_untouched and scorer_crash_siblings_untouched pass even though the starter never reads a worker name. The workers= fields are blank in those lines, and the finished script fills them.
Symptoms to watch for while working follow. A policy check that prints accepted for all three probes means the contract still checks only types, which is the starter state. An empty gave_up= field where api is expected means the apply branch is not reading the event field. A violations=1 on the forged file where 2 is expected means the loop over touched is missing, so a sibling action line goes uncounted. A report whose ensemble line is missing and still exits 0 means the fallback for absent keys is not fail-closed.
Other paths
The same two questions can be answered in jq 1.7, in a small shell script that wraps two programs. On an apply file it uses -s, which the jq 1.7 manual, "Invoking jq", describes as reading the entire input stream into a large array and running the filter just once. That fits JSONL, because each line becomes one array element. On a report file it uses -R -s, so the whole text arrives as one string. The program splits it into lines, captures each key: value pair with a regular expression and builds an object. The -r flag prints strings without quotes.
The rules sit inside the jq program and are not read from a contract. This is the main difference from the Nickel and Nushell pair. A change to the policy means editing the program text, and nothing refuses a bad policy before use. The second difference is coverage. The jq program names a non-empty siblings_touched list but does not also count a sibling action line. The run log shows the effect on the forged file.
===== fixtures/forged.apply =====
kind: apply
actions: 2
siblings_touched: yes
violations: 1
siblings_touched lists scorer on api restarting
exit: 0
jq finds the sibling list and counts one violation, where the Nushell script counted two, because the second rule does not exist in the jq program. The exit line is exit: 0 even for the forged file. The manual says "Normally jq exits with 2 if there was any usage problem or system error, 3 if there was a jq program compile error, or 0 if the jq program ran." The program ran, so jq reports success regardless of what it found. A wrapper has to derive its own verdict from the printed counts. The same holds for gaveup.report, where jq prints ensemble_healthy: no and still exits 0, while the Nushell script exits 1. The shell script does make its own check for a missing file and exits 2 there, matching the Nushell convention.
The cluster side was not run. No Pod with two containers, no ReplicaSet and no OTP supervisor were started. Both are described only in the cited documentation.
Citations
- Armstrong, Joe. Making reliable distributed systems in the presence of software errors. PhD thesis, Royal Institute of Technology (KTH), Stockholm, 2003. Section 5.2.3, "And/or supervision hierarchies", and Chapter 6.
- Erlang/OTP system documentation, "Supervisor Behaviour" (https://www.erlang.org/doc/system/sup_princ.html), restart strategies and maximum restart intensity. Documentation labelled v29.1.2, fetched 2026-11-08.
- Kubernetes documentation, "Pod Lifecycle" (https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/), container restarts, sidecar containers and
RestartAllContainers. Fetched 2026-11-08. - Kubernetes documentation, "ReplicaSet" (https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/), purpose and the comparison to a process supervisor. Fetched 2026-11-08.
- Certified Kubernetes Administrator (CKA) Curriculum v1.37, Workloads and Scheduling, "Understand the primitives used to create robust, self-healing, application deployments".
- Nickel user manual, "Contracts" (https://nickel-lang.org/user-manual/contracts/), sections "Records" and "User-defined contracts". Fetched 2026-11-03. Behaviour of
nickel exportand the refusal messages observed in Nickel 1.18.0. - Nushell command reference,
from json(https://www.nushell.sh/commands/docs/from_json.html) andopen(https://www.nushell.sh/commands/docs/open.html). Fetched 2026-11-03. - Nushell command reference,
parse(https://www.nushell.sh/commands/docs/parse.html) andreduce(https://www.nushell.sh/commands/docs/reduce.html). Fetched 2026-11-07. - jq 1.7 Manual (https://jqlang.org/manual/v1.7/), "Invoking jq", options
--slurp,--raw-outputand--exit-status. Fetched 2026-11-05 and re-used 2026-11-08.