From 9b41c90e24f474766ced315c46e8dbbb4e88c846 Mon Sep 17 00:00:00 2001 From: Asgeir Frimannsson Date: Fri, 2 Oct 2026 10:16:31 +0200 Subject: [PATCH 1/2] Read the kapi 1.3 content-memory count in plan mode kapi 1.3 reports the plan's exact content-memory count as memoryExact (kapi 1.2 wrote tmExact), so the plan reader read 0 on 1.3. It now reads memoryExact and falls back to tmExact, for the totals and the per-locale table. The output plan-tm-exact becomes plan-memory-exact; plan-tm-exact stays as a deprecated alias with the same value. The step summary, the sticky PR comment and the README say "content memory" in place of "TM", and the README names setup-kapi's parse-cache input. A stub harness checks the plan outputs, summary and comment for both plan shapes. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PWc9qYQJ93y88XrN6TkMvT --- .github/workflows/test.yml | 25 ++++++- README.md | 15 +++-- action.yml | 34 ++++++---- test/check-outcomes.sh | 2 +- test/plan-output/kapi-1.2.json | 30 +++++++++ test/plan-output/kapi-1.3.json | 30 +++++++++ test/plan-outputs.sh | 115 +++++++++++++++++++++++++++++++++ 7 files changed, 226 insertions(+), 25 deletions(-) create mode 100644 test/plan-output/kapi-1.2.json create mode 100644 test/plan-output/kapi-1.3.json create mode 100755 test/plan-outputs.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14202c7..dcf19e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,11 +44,30 @@ jobs: - name: Verify plan outputs env: MISSING: ${{ steps.plan.outputs.plan-missing }} + MEMORY: ${{ steps.plan.outputs.plan-memory-exact }} + MEMORY_DEPRECATED: ${{ steps.plan.outputs.plan-tm-exact }} TOKENS: ${{ steps.plan.outputs.plan-token-estimate }} run: | - echo "plan: missing=${MISSING} tokens=${TOKENS}" + echo "plan: missing=${MISSING} memory=${MEMORY} tokens=${TOKENS}" test "${MISSING}" -gt 0 test "${TOKENS}" -gt 0 + test "${MEMORY}" -ge 0 + # plan-tm-exact is the deprecated name of plan-memory-exact. + test "${MEMORY_DEPRECATED}" = "${MEMORY}" + + test-plan-outputs: + name: "up --plan outputs for kapi 1.2 and 1.3 (kapi stub)" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + # kapi 1.3 reports the exact content-memory count as memoryExact, and + # kapi 1.2 as tmExact. The harness runs the plan step on each and checks + # the outputs, the job summary and the PR comment. + - name: Run the plan harness + run: | + yq --version + test/plan-outputs.sh test-check-gate: name: "check --ship (exit-3 contract)" @@ -383,8 +402,8 @@ jobs: test -f test/fixture/content/en_qps.json test "${STATUS}" = "no-changes" - # The README sends callers to setup-kapi's cache-tm for caching rather than a - # cache step of their own. The save job runs the action and finds kapi's parse + # The README sends callers to setup-kapi's parse cache rather than a cache + # step of their own. The save job runs the action and finds kapi's parse # cache where setup-kapi saves it, and nothing at the retired .kapi/cache path; # the restore job must find that cache in place before the action runs again. test-parse-cache-save: diff --git a/README.md b/README.md index 290c3bc..7825258 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Passes repeat until every language clears its gate, a pass makes no progress, or flowchart LR S[source changes] --> U[kapi up] subgraph PASS ["each pass, per language behind its gate"] - TM["1 · reuse
TM exact matches"] --> AI["2 · translate
AI + terminology"] --> CK["3 · check
placeholders · terms · tags"] + MEM["1 · reuse
content memory exact matches"] --> AI["2 · translate
AI + terminology"] --> CK["3 · check
placeholders · terms · tags"] end U --> PASS CK -->|every gate met| CV["up to date
changes ready to deliver"] @@ -134,7 +134,7 @@ App token instead (both delivery actions above accept a `token:` input). ### Plan mode: the cost of a change, on its PR -`plan: "true"` dry-runs the kapi loop — pending work, TM leverage, and a token estimate, with no writes and no provider calls (so it needs no API keys). With `pr-comment: "true"` on a pull-request event, the plan lands as one sticky comment that re-runs update in place: +`plan: "true"` dry-runs the kapi loop and reports pending work, content-memory leverage and a token estimate, with no writes and no provider calls (so it needs no API keys). With `pr-comment: "true"` on a pull-request event, the plan lands as one sticky comment that re-runs update in place: ```yaml name: Translation plan @@ -157,7 +157,7 @@ jobs: pr-comment: "true" ``` -> "This change leaves **42 unit(s)** of pending translation work: 30 recoverable from TM, 12 for AI (~450 tokens estimated)." +> "This change leaves **42 unit(s)** of pending translation work: 30 recoverable from content memory, 12 for AI (~450 tokens estimated)." ### Gate pull requests on content quality @@ -287,9 +287,9 @@ context, settles, and pushes the rules it established. ### Caching -`neokapi/setup-kapi@v1` carries kapi's parse cache (`.kapi/work/cache/docs`) between runs. Its `cache-tm` input is on by default for a project with a `kapi.yaml`, so this Action needs no cache step of its own. Set setup-kapi's `project-dir` when the recipe is not at the repository root. +`neokapi/setup-kapi@v1` carries kapi's parse cache (`.kapi/work/cache/docs`) between runs. Its `parse-cache` input is on by default for a project with a `kapi.yaml`, so this Action needs no cache step of its own. Older setup-kapi releases name the input `cache-tm`, and newer ones still accept that name with a deprecation warning. Set setup-kapi's `project-dir` when the recipe is not at the repository root. -Leave the rest of `.kapi/work/` out of any cache. Its store holds the targets and review state a run produced, and a restored copy changes what `kapi status`, `kapi check --ship` and `kapi up` report. +Leave the rest of `.kapi/work/` out of any cache. Its store holds the targets a run produced, and a restored copy changes what `kapi status`, `kapi check --ship` and `kapi up` report. ## Inputs @@ -298,7 +298,7 @@ Leave the rest of `.kapi/work/` out of any cache. Its store holds the targets an | `command` | `up` | Kapi subcommand to execute | | `args` | | Additional arguments | | `project` | | Path to the `kapi.yaml` recipe (`-p` flag) | -| `plan` | `false` | With `command: up`: dry run — pending work, TM leverage, token estimate; no writes, no provider calls | +| `plan` | `false` | With `command: up`: a dry run reporting pending work, content-memory leverage and a token estimate; no writes, no provider calls | | `fail-on-parked` | `false` | With `command: up`, fail the workflow when the run parks instead of reporting partial progress | | `pr-comment` | `false` | Sticky report comment on pull-request events, including for a failed gate or a check that did not run | | `token` | `${{ github.token }}` | Token for the sticky PR comment | @@ -316,7 +316,8 @@ Leave the rest of `.kapi/work/` out of any cache. Its store holds the targets an | `gate` | With `command: check`: `pass` or `fail` (empty when the check did not run or errored) | | `result` | With `command: check`: `passed` (exit 0), `failed` (exit 3), `did_not_run` (exit 4), or `error` (any other exit code) | | `did-not-run-cause` | With `command: check`, when `result` is `did_not_run`: `checker_invalid`, `nothing_to_check`, `content_not_checked`, or `unknown` | -| `plan-missing` / `plan-tm-exact` / `plan-ai-remaining` / `plan-token-estimate` | With `plan: true`: the plan totals | +| `plan-missing` / `plan-memory-exact` / `plan-ai-remaining` / `plan-token-estimate` | With `plan: true`: the plan totals; `plan-memory-exact` counts the units recoverable from exact content-memory matches | +| `plan-tm-exact` | Deprecated: use `plan-memory-exact`, which holds the same value | | `has-changes` | Whether the run left changes in the working tree for your delivery step | | `changed-files` | Newline-separated paths the run changed | diff --git a/action.yml b/action.yml index 7a602bf..49dfba2 100644 --- a/action.yml +++ b/action.yml @@ -19,8 +19,9 @@ inputs: default: "" plan: description: >- - With `command: up`, dry-run instead: report pending work, TM leverage, - and a token estimate without writing anything or calling a provider. + With `command: up`, dry-run instead: report pending work, content-memory + leverage, and a token estimate without writing anything or calling a + provider. Pairs with `pr-comment` to post the cost of a change on its PR. required: false default: "false" @@ -87,9 +88,12 @@ outputs: plan-missing: description: "With `plan: true`: pending units across all locales" value: ${{ steps.run-kapi.outputs.plan-missing }} + plan-memory-exact: + description: "With `plan: true`: units recoverable from exact content-memory matches" + value: ${{ steps.run-kapi.outputs.plan-memory-exact }} plan-tm-exact: - description: "With `plan: true`: units recoverable from TM exact hits" - value: ${{ steps.run-kapi.outputs.plan-tm-exact }} + description: "Deprecated: use `plan-memory-exact`, which holds the same value" + value: ${{ steps.run-kapi.outputs.plan-memory-exact }} plan-ai-remaining: description: "With `plan: true`: units left for AI translation" value: ${{ steps.run-kapi.outputs.plan-ai-remaining }} @@ -146,28 +150,30 @@ runs: printf 'Running:'; printf ' %q' "${cmd[@]}"; echo "${cmd[@]}" > "${RUNNER_TEMP}/kapi-plan.json" MISSING=$(jq -r '.totals.missingTarget // 0' "${RUNNER_TEMP}/kapi-plan.json") - TM=$(jq -r '.totals.tmExact // 0' "${RUNNER_TEMP}/kapi-plan.json") + # kapi 1.3 names the exact content-memory count memoryExact; kapi 1.2 + # wrote the same number as tmExact. + MEMORY=$(jq -r '.totals.memoryExact // .totals.tmExact // 0' "${RUNNER_TEMP}/kapi-plan.json") AI=$(jq -r '.totals.aiRemaining // 0' "${RUNNER_TEMP}/kapi-plan.json") TOKENS=$(jq -r '.totals.tokenEstimate // 0' "${RUNNER_TEMP}/kapi-plan.json") { echo "mode=plan" echo "plan-missing=${MISSING}" - echo "plan-tm-exact=${TM}" + echo "plan-memory-exact=${MEMORY}" echo "plan-ai-remaining=${AI}" echo "plan-token-estimate=${TOKENS}" } >> "$GITHUB_OUTPUT" { echo "## kapi up — plan" echo - echo "This change leaves **${MISSING} unit(s)** of pending translation work: ${TM} recoverable from TM, ${AI} for AI (~${TOKENS} tokens estimated)." + echo "This change leaves **${MISSING} unit(s)** of pending translation work: ${MEMORY} recoverable from content memory, ${AI} for AI (~${TOKENS} tokens estimated)." echo - echo "| Locale | Pending | TM exact | AI remaining | Token estimate |" + echo "| Locale | Pending | Content memory exact | AI remaining | Token estimate |" echo "|---|---|---|---|---|" - jq -r '.scopes[] | "| \(.locale) | \(.missingTarget) | \(.tmExact) | \(.aiRemaining) | \(.tokenEstimate) |"' "${RUNNER_TEMP}/kapi-plan.json" + jq -r '.scopes[] | "| \(.locale) | \(.missingTarget) | \(.memoryExact // .tmExact // 0) | \(.aiRemaining) | \(.tokenEstimate) |"' "${RUNNER_TEMP}/kapi-plan.json" echo echo "_Estimate is source-chars/4; no provider calls were made._" } >> "$GITHUB_STEP_SUMMARY" - echo "Plan: ${MISSING} unit(s) pending, ${TM} from TM, ${AI} for AI (~${TOKENS} tokens)." + echo "Plan: ${MISSING} unit(s) pending, ${MEMORY} from content memory, ${AI} for AI (~${TOKENS} tokens)." exit 0 fi @@ -426,7 +432,7 @@ runs: CAUSE: ${{ steps.run-kapi.outputs.did-not-run-cause }} DID_NOT_RUN_SUMMARY: ${{ steps.run-kapi.outputs.did-not-run-summary }} PLAN_MISSING: ${{ steps.run-kapi.outputs.plan-missing }} - PLAN_TM: ${{ steps.run-kapi.outputs.plan-tm-exact }} + PLAN_MEMORY: ${{ steps.run-kapi.outputs.plan-memory-exact }} PLAN_AI: ${{ steps.run-kapi.outputs.plan-ai-remaining }} PLAN_TOKENS: ${{ steps.run-kapi.outputs.plan-token-estimate }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -435,13 +441,13 @@ runs: MARKER="" if [ "$MODE" = "plan" ]; then - TABLE=$(jq -r '.scopes[] | "| \(.locale) | \(.missingTarget) | \(.tmExact) | \(.aiRemaining) | \(.tokenEstimate) |"' "${RUNNER_TEMP}/kapi-plan.json" 2>/dev/null || true) + TABLE=$(jq -r '.scopes[] | "| \(.locale) | \(.missingTarget) | \(.memoryExact // .tmExact // 0) | \(.aiRemaining) | \(.tokenEstimate) |"' "${RUNNER_TEMP}/kapi-plan.json" 2>/dev/null || true) BODY="${MARKER} ## kapi up — plan - This change leaves **${PLAN_MISSING:-0} unit(s)** of pending translation work: ${PLAN_TM:-0} recoverable from TM, ${PLAN_AI:-0} for AI (~${PLAN_TOKENS:-0} tokens estimated). + This change leaves **${PLAN_MISSING:-0} unit(s)** of pending translation work: ${PLAN_MEMORY:-0} recoverable from content memory, ${PLAN_AI:-0} for AI (~${PLAN_TOKENS:-0} tokens estimated). - | Locale | Pending | TM exact | AI remaining | Token estimate | + | Locale | Pending | Content memory exact | AI remaining | Token estimate | |---|---|---|---|---| ${TABLE} diff --git a/test/check-outcomes.sh b/test/check-outcomes.sh index 8d46a5f..db72b29 100755 --- a/test/check-outcomes.sh +++ b/test/check-outcomes.sh @@ -115,7 +115,7 @@ comment_case() { GATE="$(output_value gate "$out")" RESULT="$(output_value result "$out")" \ CAUSE="$(output_value did-not-run-cause "$out")" \ DID_NOT_RUN_SUMMARY="$(output_value did-not-run-summary "$out")" \ - PLAN_MISSING="" PLAN_TM="" PLAN_AI="" PLAN_TOKENS="" \ + PLAN_MISSING="" PLAN_MEMORY="" PLAN_AI="" PLAN_TOKENS="" \ RUNNER_TEMP="$dir/tmp" \ bash "$work/comment.sh" > "$dir/comment.stdout" 2>&1 || rc=$? diff --git a/test/plan-output/kapi-1.2.json b/test/plan-output/kapi-1.2.json new file mode 100644 index 0000000..1d94935 --- /dev/null +++ b/test/plan-output/kapi-1.2.json @@ -0,0 +1,30 @@ +{ + "flow": "default (built-in)", + "flowDrafts": true, + "flowCallsProvider": true, + "scopes": [ + { + "locale": "de", + "collection": "app", + "missingTarget": 3, + "tmExact": 1, + "aiRemaining": 2, + "tokenEstimate": 7 + }, + { + "locale": "fr", + "collection": "app", + "missingTarget": 3, + "tmExact": 2, + "aiRemaining": 1, + "tokenEstimate": 4 + } + ], + "totals": { + "missingTarget": 6, + "tmExact": 3, + "aiRemaining": 3, + "tokenEstimate": 11 + }, + "note": "content-memory leverage counts exact-hash hits only; token estimate is source chars / 4 for the remaining units (no tokenizer, no provider calls)." +} diff --git a/test/plan-output/kapi-1.3.json b/test/plan-output/kapi-1.3.json new file mode 100644 index 0000000..dfc9c5e --- /dev/null +++ b/test/plan-output/kapi-1.3.json @@ -0,0 +1,30 @@ +{ + "flow": "default (built-in)", + "flowDrafts": true, + "flowCallsProvider": true, + "scopes": [ + { + "locale": "de", + "collection": "app", + "missingTarget": 3, + "memoryExact": 1, + "aiRemaining": 2, + "tokenEstimate": 7 + }, + { + "locale": "fr", + "collection": "app", + "missingTarget": 3, + "memoryExact": 2, + "aiRemaining": 1, + "tokenEstimate": 4 + } + ], + "totals": { + "missingTarget": 6, + "memoryExact": 3, + "aiRemaining": 3, + "tokenEstimate": 11 + }, + "note": "content-memory leverage counts exact-hash hits only; token estimate is source chars / 4 for the remaining units (no tokenizer, no provider calls)." +} diff --git a/test/plan-outputs.sh b/test/plan-outputs.sh new file mode 100755 index 0000000..9803a7b --- /dev/null +++ b/test/plan-outputs.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Runs the action's "Run kapi" step in plan mode against the kapi stub in +# test/stub, once with the plan kapi 1.3 prints (memoryExact) and once with the +# plan kapi 1.2 printed (tmExact), and asserts the outputs, the job summary and +# the sticky PR comment each produces. The step scripts are read out of +# action.yml, so the code under test is the code the action ships. Needs yq +# (v4) and jq. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(dirname "$here")" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +yq '.runs.steps[] | select(.id == "run-kapi") | .run' "$root/action.yml" > "$work/step.sh" +if ! grep -q 'kapi-plan.json' "$work/step.sh"; then + echo "could not read the run-kapi step out of action.yml" >&2 + exit 1 +fi +yq '.runs.steps[] | select(.id == "pr-comment") | .run' "$root/action.yml" > "$work/comment.sh" +if ! grep -q 'kapi-report' "$work/comment.sh"; then + echo "could not read the pr-comment step out of action.yml" >&2 + exit 1 +fi + +failures=0 + +output_value() { + sed -n "s/^$1=//p" "$2" | tail -n 1 +} + +fail() { + echo "FAIL $1: $2" + failures=$((failures + 1)) +} + +# contains NAME FILE TEXT asserts that FILE contains TEXT. +contains() { + grep -qF -- "$3" "$2" || fail "$1" "$(basename "$2") lacks '$3'" +} + +# plan_case NAME OUTPUT runs the plan step with the stub printing +# test/plan-output/OUTPUT, then the sticky PR comment step with what it wrote. +plan_case() { + local name="$1" output="$2" + local dir="$work/cases/$name" + mkdir -p "$dir/tmp" + : > "$dir/output" + : > "$dir/summary" + local before="$failures" + + local rc=0 + env \ + PATH="$here/stub:$PATH" \ + COMMAND=up ARGS="" PROJECT="" PLAN=true FAIL_ON_PARKED=false \ + RUNNER_TEMP="$dir/tmp" GITHUB_OUTPUT="$dir/output" GITHUB_STEP_SUMMARY="$dir/summary" \ + KAPI_STUB_OUTPUT="$here/plan-output/$output" KAPI_STUB_EXIT=0 KAPI_STUB_ARGV="$dir/argv" \ + bash "$work/step.sh" > "$dir/stdout" 2>&1 || rc=$? + [ "$rc" = 0 ] || fail "$name" "exit code: got '$rc', want '0'" + + local got + for pair in mode=plan plan-missing=6 plan-memory-exact=3 plan-ai-remaining=3 plan-token-estimate=11; do + got="$(output_value "${pair%%=*}" "$dir/output")" + [ "$got" = "${pair#*=}" ] || fail "$name" "${pair%%=*}: got '$got', want '${pair#*=}'" + done + got="$(tr '\n' ' ' < "$dir/argv")" + [ "$got" = "up --plan --json " ] || fail "$name" "kapi argv: got '$got', want 'up --plan --json '" + + contains "$name" "$dir/summary" "3 recoverable from content memory, 3 for AI (~11 tokens estimated)." + contains "$name" "$dir/summary" "| Locale | Pending | Content memory exact | AI remaining | Token estimate |" + contains "$name" "$dir/summary" "| de | 3 | 1 | 2 | 7 |" + contains "$name" "$dir/summary" "| fr | 3 | 2 | 1 | 4 |" + contains "$name" "$dir/stdout" "Plan: 6 unit(s) pending, 3 from content memory, 3 for AI (~11 tokens)." + + rc=0 + env \ + PATH="$here/stub:$PATH" GH_STUB_LOG="$dir/gh" \ + GH_TOKEN=unused REPO=neokapi/kapi-action PR_NUMBER=1 \ + RUN_URL="https://github.com/neokapi/kapi-action/actions/runs/1" \ + MODE="$(output_value mode "$dir/output")" OUTCOME="" PASSES="" PARKED="" \ + GATE="" RESULT="" CAUSE="" DID_NOT_RUN_SUMMARY="" \ + PLAN_MISSING="$(output_value plan-missing "$dir/output")" \ + PLAN_MEMORY="$(output_value plan-memory-exact "$dir/output")" \ + PLAN_AI="$(output_value plan-ai-remaining "$dir/output")" \ + PLAN_TOKENS="$(output_value plan-token-estimate "$dir/output")" \ + RUNNER_TEMP="$dir/tmp" \ + bash "$work/comment.sh" > "$dir/comment.stdout" 2>&1 || rc=$? + [ "$rc" = 0 ] || fail "$name comment" "exit code: got '$rc', want '0'" + if [ ! -f "$dir/gh/body.md" ]; then + fail "$name comment" "posted no comment" + else + contains "$name comment" "$dir/gh/body.md" "This change leaves **6 unit(s)** of pending translation work: 3 recoverable from content memory, 3 for AI (~11 tokens estimated)." + contains "$name comment" "$dir/gh/body.md" "| Locale | Pending | Content memory exact | AI remaining | Token estimate |" + contains "$name comment" "$dir/gh/body.md" "| de | 3 | 1 | 2 | 7 |" + contains "$name comment" "$dir/gh/body.md" "| fr | 3 | 2 | 1 | 4 |" + fi + + if [ "$failures" = "$before" ]; then + echo "ok $name" + else + sed 's/^/ | /' "$dir/stdout" "$dir/summary" + if [ -f "$dir/gh/body.md" ]; then + sed 's/^/ > /' "$dir/gh/body.md" + fi + fi +} + +plan_case kapi-1.3 kapi-1.3.json +plan_case kapi-1.2 kapi-1.2.json + +if [ "$failures" -gt 0 ]; then + echo "$failures assertion(s) failed" + exit 1 +fi +echo "all plan outputs passed" From 0ece73ef5953a035df8780284b30343551700dfd Mon Sep 17 00:00:00 2001 From: Asgeir Frimannsson Date: Fri, 2 Oct 2026 10:17:20 +0200 Subject: [PATCH 2/2] Describe the action in content-engine terms The action description and the README's opening line say the action runs kapi commands that bring a project's content and its translations up to date, gate content quality and plan cost, and report what changed to a delivery step the workflow owns. The README's loop section now matches kapi 1.3: reuse fills from content memory, translation carries the voice context, a unit with a failing finding counts as translated and holds its locale out of shipping, and an approval is recorded in the project's context store and shared with kapi context push. kapi 1.3 has no kapi commit command. The gate summary names a voice gate, and a comment says target-language content. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PWc9qYQJ93y88XrN6TkMvT --- README.md | 12 ++++++------ action.yml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7825258..3e9b7b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kapi Action -A GitHub Action that runs [kapi](https://github.com/neokapi/neokapi) commands — catch up translations, gate content quality, plan cost — and delivers the results — as a commit, a pull request, or a report on the PR that caused the work. +A GitHub Action that runs [kapi](https://github.com/neokapi/neokapi) commands that bring a project's content and its translations up to date, gate content quality and plan the cost of a change. It reports what changed to a delivery step you own (a commit or a pull request), and can post a report on the pull request that caused the work. ## Prerequisites @@ -67,9 +67,9 @@ Parked is the interesting one: partial progress is real progress, so the default `kapi up` treats the recipe as the desired state — the languages the project targets, and the ship gates that define *shippable* — and reconciles the content toward it. Each pass, for every language behind its gate: -1. **Reuse** — exact translation-memory matches fill first, for free. -2. **Translate** — the configured AI provider fills what remains, with the project's terminology and brand context. -3. **Check** — deterministic checks run over what was produced (placeholder integrity, inline tags, do-not-translate terms, untranslated text). A unit with a failing finding counts as *drafted*, not translated — it cannot clear a gate until fixed. +1. **Reuse**: exact content-memory matches fill first, for free. +2. **Translate**: the configured AI provider fills what remains, with the project's terminology and voice context. +3. **Check**: deterministic checks run over what was produced (placeholder integrity, inline tags, do-not-translate terms, untranslated text). A unit with a failing finding still counts as *translated*, and the finding holds its locale out of shipping until it is fixed. Passes repeat until every language clears its gate, a pass makes no progress, or the pass cap is reached. @@ -82,11 +82,11 @@ flowchart LR U --> PASS CK -->|every gate met| CV["up to date
changes ready to deliver"] CK -->|needs a person| PK["parked
the review queue"] - PK --> RV["review & approve
committed under .kapi/state"] + PK --> RV["review & approve
recorded in the project's context"] RV -.->|next run sees it| U ``` -**Parked work is the review queue, not an error.** What the machine couldn't decide waits for a person: review the wording, approve or fix it, and `kapi commit` records the decision under `.kapi/state/`, or the connected server records it. Approvals raise the `reviewed` coverage the ship gate measures, so the next run and the next gate see them. `kapi check --ship` (see [Gate pull requests](#gate-pull-requests-on-content-quality)) is what enforces the bar at release time. +**Parked work is the review queue, not an error.** What the machine couldn't decide waits for a person: review the wording, approve or fix it, and the approval is recorded in the project's context (with `kapi apply` or the Review page of Kapi Desktop), or the connected server records it. Push it with `kapi context push`, and a run with `context-sync` takes it in. Approvals raise the `established` coverage the ship gate measures, so the next run and the next gate see them. `kapi check --ship` (see [Gate pull requests](#gate-pull-requests-on-content-quality)) is what enforces the bar at release time. The kapi up report (outcome, passes, parked locales) is always written to the job summary. Under the hood the Action runs `kapi up --json`, an NDJSON stream — one convergence event per line, closed by a single `{"type":"result", ...}` record. That record is the contract; the events are the log. It becomes the `outcome`, `passes`, and `parked-locales` outputs. diff --git a/action.yml b/action.yml index 49dfba2..3259b4d 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ name: "Kapi Run" -description: "Run kapi commands — catch up translations, gate content quality, plan cost — and report what changed. Delivery (commit, push, PR) is your workflow's own step" +description: "Run kapi commands that bring a project's content and its translations up to date, gate content quality and plan cost, and report what changed. Delivery (commit, push, PR) is your workflow's own step" branding: icon: "refresh-cw" color: "blue" @@ -203,7 +203,7 @@ runs: { echo "## kapi check" echo - echo "❌ **Gate: unmet** — a brand, terminology, QA, or coverage gate failed. See the findings in the job log." + echo "❌ **Gate: unmet**. A voice, terminology, QA or coverage gate failed. See the findings in the job log." } >> "$GITHUB_STEP_SUMMARY" echo "::error::kapi gate unmet (exit 3) — content quality gates failed; read the findings and fix them. This is 'not on-spec yet', not a crash." exit 3 @@ -386,7 +386,7 @@ runs: if [ -z "$PATHS" ]; then # No paths specified — check the entire working tree. `kapi init` # gitignores the .kapi/ state directory, so what shows up here is the - # localized content the run produced, including brand-new locale files + # target-language content the run produced, including brand-new locale files # (which a tracked-files-only diff would miss). DIRT="$(git status --porcelain)" else