23 KiB
Upstream Sync and Preventing Translation Drift (Maintainer Guide)
한국어 · English
This document lays out the procedure a maintainer follows to keep up with changes in the upstream repository Autumn-27/ARTEX while maintaining the Korean localization. Contribution scope, legal responsibility, and the localization policy live in CONTRIBUTING.en.md; user-facing guidance lives in README.en.md. This document therefore focuses solely on how that policy is actually enforced.
The core goal of the localization can be summed up in one sentence: preserve the original's judgment performance exactly, while translating only the user-facing output into Korean. That boundary is easy to blur every time upstream updates, so the procedures and checks below prevent translation drift.
1. The localization structure at a glance
This repository forks upstream ARTEX and stacks Korean localization commits on top of its
history. Every commit on upstream main is contained in this repository's history, with the
localization commits added above them. Bringing in an upstream change therefore becomes a matter of
"inspecting the difference against upstream main, then separating what to preserve from what to
translate and applying each accordingly."
The deliverables fall into three groups.
- Assets kept in the original language (section 2). Translating them breaks performance or the ability to diff against upstream.
- Output-language enforcement fixed in code.
langDirective()inagent/prompt.goappends to the end of each role's system prompt the instruction "write user-facing output in Korean." - User-facing strings that are translated into Korean. The UI lives in
web/messages/ko.json; the server's user-facing response strings live in named constants in each Go file.
2. Assets preserved in the original language (do not translate)
The following assets keep their original language (Chinese or English) and are not translated. When an upstream change touches these assets, apply it as is, without translating.
- The agent's internal reasoning prompts (the "brain" body). These are the behavioral-instruction
bodies in
agent/promptcatalog.goand the DB seedagent_prompts. The behavior was benchmarked against the original (Chinese), so translating it introduces drift in the agent's judgment. - Strings that serve both as display and as agent input. Some text that appears on the activity
timeline while also being fed back as planner/reporter input context (task-abort reasons,
interception-block messages, traffic-evidence helpers, and so on) is kept in the original, because
a single record serves two purposes. The rationale is recorded case by case under the "brain
boundary records" in
work/DECISIONS-FOR-JIWOO.md. - The original Chinese documents and strings. Documents keep the original in
README.zh.mdand UI strings keep it inweb/messages/zh.json, so that diffing against upstream changes stays easy. Korean translations are filled in only inweb/messages/ko.json. - Command, payload, code, URL, identifier, and log originals. These are the originals needed for analysis, so they are not translated. Go code comments are the lowest priority as well and stay in the original until the upstream diff is finished.
3. The upstream tracking base
The upstream remote must be configured as follows. If it is missing, add it.
git remote add upstream https://github.com/Autumn-27/ARTEX
git remote -v # check that upstream shows up
The upstream base commit that the localization has finished applying is:
- Base =
d003372(upstreammain, 2026-10-03, merge of PR #189fix/sse-same-origin).
This value means "every upstream change up to this commit is already folded into this repository." Each time you apply a new upstream change, update this base using the method in section 7.
4. The procedure for bringing in upstream changes
4.1 Fetch upstream and inspect the difference
git fetch upstream
git rev-list --count d003372..upstream/main # number of unapplied upstream commits
git log --oneline d003372..upstream/main # list of unapplied commits
git fetch only updates upstream's remote-tracking branch, so it leaves the working tree and HEAD
untouched. If the count of unapplied commits is 0, you are in sync with upstream and there is nothing
more to do.
4.2 Classify the changed files
See which files the unapplied commits touched, and split them into the preserved assets of section 2 and the translation targets.
git log --name-status --oneline d003372..upstream/main
The classification criteria are as follows.
- If
agent/promptcatalog.go/ theagent_promptsseed, or the display-and-input strings of section 2, changed → apply as is, without translating. - If Go backend logic (
db/,llmrec/,server/, and so on) changed → apply the logic as is, but check for newly introduced user-facing strings (writeErr, and the like) and translate those into Korean constants. - If the UI (
web/src/**) changed and introduced new screen strings → do not hard-code them; add them under the same key toweb/messages/zh.json(original) andweb/messages/ko.json(translation). - If upstream indicators pinned by the detection rules changed (the prober User-Agent in
enrich/enrich.go, the self-update User-Agent inselfupdate/, the audit marker inguard/guard.go, the destructive-command deny list indb/db.go, the default listen and recording-proxy ports incmd/artex/main.go) → bring the Sigma/Suricata rules and the ATT&CK layer indetections/, as well as the values indetections/indicators/artex_indicators.csv, in line with the new values. These indicators are not translation targets but the basis of detection, so when upstream changes a value, the rules silently go stale. The indicator-match test in 5.4 catches that mismatch automatically.
4.3 Apply
Merge or cherry-pick feature by feature, then translate the new strings you separated out in 4.2 into
Korean. During the merge it is easy for ko.json / zh.json keys to fall out of sync or for an
original string to leak into a user-facing slot, so always run the checks in section 5 right after
applying.
Example (unapplied commits as of 2026-10-05). The
git fetch upstreamresult shows upstreammainahead atb55ceb1, with 2 commits (86729b6, the model-fallback approval-token metering feature, plus the merge commitb55ceb1) unapplied relative to based003372. These commits touch Go logic such asdb/llm_usage.go,llmrec/llmrec.go,server/intercept.go, andserver/server.go, andweb/src/app/(main)/system/intercept/page.tsx,web/src/lib/api.ts,web/src/lib/mock/handler.ts, andweb/src/lib/types.ts. The maintainer therefore applies the Go logic as is and only extracts and translates the new screen strings introduced on the intercept settings page intoko.json/zh.jsonkeys. (These two commits had not yet been applied at the time this document was written, so the base stays atd003372.)
5. Translation-symmetry and drift checks
After applying an upstream change or doing translation work, verify the following three things.
5.1 ko ↔ zh message symmetry and user-facing CJK
The keys of ko.json and zh.json must be exactly the same, and no Chinese characters may remain in
the ko.json values. The script below prints three numbers.
python3 - <<'PY'
import json, re
ko = json.load(open('web/messages/ko.json'))
zh = json.load(open('web/messages/zh.json'))
def flatten(d, p=''):
out = {}
if isinstance(d, dict):
for k, v in d.items(): out.update(flatten(v, p + '/' + k))
elif isinstance(d, list):
for i, v in enumerate(d): out.update(flatten(v, p + '/' + str(i)))
else: out[p] = d
return out
fk, fz = flatten(ko), flatten(zh)
han = re.compile(r'[㐀-鿿]')
print('ko leaf keys :', len(fk))
print('zh leaf keys :', len(fz))
print('key symdiff :', len(set(fk) ^ set(fz))) # must be 0
print('ko vals w/CJK:', sum(1 for v in fk.values() if isinstance(v, str) and han.search(v))) # must be 0
PY
Baseline (2026-10-05): ko leaf keys = 2950, zh leaf keys = 2950, key symdiff = 0,
ko vals w/CJK = 0. The key count can grow as upstream changes are applied, but ko and zh must always
be equal, and key symdiff and ko vals w/CJK must always be 0.
5.2 Confirm that brain assets keep their original language
The brain body keeps the original Chinese, so if the count of Han-character lines drops to 0 in the check below, that is a signal that the brain was accidentally translated and contaminated.
python3 -c "import re; han=re.compile(r'[㐀-鿿]'); t=open('agent/promptcatalog.go').read(); print('promptcatalog.go CJK lines =', sum(1 for l in t.splitlines() if han.search(l)))"
Baseline (2026-10-05): promptcatalog.go CJK lines = 70. If this number drops sharply, check whether
the brain body was translated.
5.3 Make sure no original language leaks into the build output
After exporting the UI statically, Chinese appearing in the prerendered HTML means a missing translation.
cd web && npm ci && NEXT_EXPORT=1 npm run build # generates out/
# check that the visible text in out/**/*.html contains 0 Chinese characters
5.4 Make sure the detection indicators still match the upstream source
The rules in detections/ are based on the strings that upstream actually emits (prober User-Agent,
self-update User-Agent, audit marker, destructive-command deny list). When an upstream re-sync changes
these values, the translation checks all pass while only the shipped rules silently stop matching. The
test below verifies bidirectionally that each indicator is still present in both the upstream source
and the rules, so run it after a re-sync.
detections/tests/indicators/run.sh # runs in isolation under Docker; RESULT: PASS means a match
On failure it prints which indicator is out of sync and in which direction (whether the upstream
source changed or the rule changed), so bring the rules and the layer in line with the new values per
the last classification criterion of 4.2. This test also runs automatically in the repository CI
(.github/workflows/detections.yml) on every push/PR that changes
the rule tree or the upstream source files above, catching re-sync drift at the merge gate.
If you add a new indicator and in doing so pin a new upstream source file (as when adding the port
indicator from cmd/artex/main.go, for example), you must also add that file to the push and
pull_request paths filters of the workflow above. If you miss it, a PR that changes only that
source will not trigger the indicator test, and the drift will silently pass the merge gate. The
indicator test checks this synchronization itself (its fifth check, "CI triggers this test when any
pinned source changes"): if any non-detections/ source the test reads is not listed in both paths
blocks, the test fails, so source pinning and CI trigger conditions cannot be merged out of sync.
5.5 When bumping the detection-test tool pins
The detection tests run sigma-cli, the SigmaHQ validator plugin (pySigma-validators-sigmahq), and
the Suricata image at fixed versions (the defaults in each run.sh, overridable via environment
variables). Bumping these pins can introduce tool-side drift rather than upstream-source drift. In
particular, the SigmaHQ validator adds new convention checks with each release, so
detections/tests/sigma_lint/run.sh may surface new issues in red. When that happens, bring the rules
in line with the new convention, or — if the convention does not fit a standalone rule set — record
the reason and add it to the exclusion list in
detections/tests/sigma_lint/validators.yml. If a
backend plugin changes its support, the sigma_backends test gives the same signal.
6. Finishing with build and test verification
After applying and translating, verify the backend and frontend per the development environment procedure in CONTRIBUTING.en.md. If you do not have Go locally, you can run the same thing under Docker.
docker run --rm -v "$PWD":/src -w /src \
-v artexko-gomod:/go/pkg/mod -v artexko-gocache:/root/.cache/go-build \
golang:1.26 sh -c 'go build ./... && go vet ./... && go test ./... -count=1'
When you translate a user-facing string, add a regression test (*_localized_test.go) that asserts
that string as well, so that if a later upstream change pulls Chinese back in, the test catches it.
Always do translation verification with a capable (frontier-class) model. Low-cost, small models can
revert their output to the original language, so you must not judge whether a translation applied from
their output alone.
7. Base-update record
Once you have applied an upstream change and finished verifying it, update the base commit value in section 3 of this document to the new upstream commit and include that change in the same commit or a following one. Doing so lets the next maintainer confirm "how far things have been applied" in this one place.
Commit messages follow the commit-message rules in CONTRIBUTING.en.md. For example, an upstream-sync commit is written like this (the description is in Korean, per this repository's actual rule).
chore(upstream): 상류 d003372..b55ceb1 반영 (intercept 토큰 계량) + 신규 UI 문자열 번역
8. Review and verification rules of thumb (common pitfalls)
Here are two pitfalls maintainers repeatedly fall into when checking upstream applies, translations, and documentation improvements. Both are cases where "the checking method itself is wrong, so you mistake something healthy for broken," so they are fixed here as rules of thumb to prevent unnecessary reverts.
8.1 Check the repository's CI status by specifying the repository
This repository is a fork of upstream ARTEX, so the local git remote has both origin
(jiwoochris/artex-ko) and upstream (Autumn-27/ARTEX) registered (see section 3). In this state, if
you do not specify a repository in a gh command, gh picks the upstream repository as the
default and shows you run results from upstream, which does not have our workflows. You can then see
upstream CI green and mistakenly think our CI passed, or judge our workflows (ci.yml,
detections.yml) as "HTTP 404 ... not found" by mistake.
So when checking CI, always name the repository explicitly.
gh run list -R jiwoochris/artex-ko --workflow ci.yml --limit 5
gh run list -R jiwoochris/artex-ko --workflow detections.yml --limit 5
Once set, you can make gh default to our repository even when you omit -R. Note, however, that
this setting is a local gh setting and is not committed to the repository, so you must set it
again on a new machine or a new checkout.
gh repo set-default jiwoochris/artex-ko
gh repo set-default --view # check that jiwoochris/artex-ko shows up
8.2 Check external links in documents with GET, like a browser
Section 7 of the defense guide (docs/defense-en.md ·
defense-ko.md) carries links to Korean official channels (boho.or.kr,
fsec.or.kr, pipc.go.kr). When checking whether these links are alive, using only curl -I (a HEAD
request) or the default User-Agent will mistake a healthy link for a broken one. Korean public and
security agency sites refuse a simple check for three reasons.
- They reject HEAD requests. For example, fsec.or.kr returns 400 to
curl -I(HEAD). - They block the default
curlUser-Agent. fsec.or.kr and pipc.go.kr return 400 even to GET requests sent with the default UA (they return 200 when sent with a browser UA). - They redirect to a different address. pipc.go.kr redirects twice, from
www.pipc.go.krtopipc.go.kr/np/, so if you do not follow redirects you miss the final status.
So check links with a browser User-Agent, with GET, following redirects.
UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0 Safari/537.36'
for u in https://www.boho.or.kr https://www.fsec.or.kr https://www.pipc.go.kr; do
curl -sS -L -A "$UA" -o /dev/null -w "$u -> %{http_code} %{url_effective}\n" "$u"
done
A final status code of 200 means the link is valid. If the status code comes back 400 or 403, first suspect that the link is not broken but that your checking method was blocked by the server's access policy, and re-check by eliminating factors one at a time: HEAD, the default UA, and not following redirects. (As of the 2026-10-06 check, all three links return 200 with the method above, with pipc.go.kr returning 200 after two redirects.)
This manual procedure is automated as-is by scripts/check-external-links.py. It gathers the external
links outside code fences and inline code from every tracked .md (excluding reserved and placeholder
hosts), checks their status with a browser UA, GET, and redirect-following as above, and retries on
network errors, 5xx, and 429 to separate transient flakes from real outages. It sorts results into four
classes: OK (2xx/3xx) · RESTRICTED (401/403/405/429 — the host is alive, only the checking method is
blocked) · ALLOWED (a known upstream-inherited dead link we cannot fix, listed in
scripts/external-links-allowlist.txt) · DOWN (404/410/5xx/connection error — likely broken).
- Preview the targets without the network:
python3 -I scripts/check-external-links.py --list - Release/periodic check (exits non-zero on a newly broken link):
python3 -I scripts/check-external-links.py --strict
External-link liveness is flaky, so it is not a merge gate. Instead the non-blocking
external-links workflow runs --strict every Monday and on
manual dispatch, turning red when a DOWN not on the allowlist newly appears. Dead links inherited by
upstream-preserved files (for example a vanished contributor account credited in CHANGELOG.zh.md) are
ones we cannot fix, so they go on the allowlist with a reason and are excluded from the strict check.
9. The release pipeline
Pushing a version tag (v*) makes .github/workflows/release.yml
build binaries for five platforms and, when a condition is met, a multi-architecture Docker image.
This fork has never cut a release tag, so this workflow has never run. This section therefore records
what the pipeline assumes and produces, and whether those assumptions match the current repository
structure. Because pushing a tag creates a GitHub Release on the public repository, cut a release only
after the publishing decision is made.
9.1 How to cut a release
Pushing a tag that starts with v fires the workflow.
git tag v0.3.15
git push origin v0.3.15
9.2 What the pipeline does
The workflow is split into five jobs.
- frontend. Statically exports the frontend once (
web/out) and uploads that output as theweb-distartifact. The binaries job below downloads and reuses this output per target. - binaries. Cross-compiles five targets (linux amd64/arm64, darwin amd64/arm64, windows amd64) and
packages a zip per target. On the linux amd64 binary it runs an
artex -hsmoke test to confirm the binary actually runs. - release. Gathers all zips, generates a
SHA256SUMSchecksum file, and creates a GitHub Release with the zips and the checksum attached. - docker-gate. Checks whether the
DOCKERHUB_USERNAME/DOCKERHUB_TOKENsecrets are set and passes that result on as the run condition for the next job. - docker. Runs only when those secrets exist; it takes the linux binaries cross-compiled by binaries, builds a multi-architecture image, and pushes it to Docker Hub. When the secrets are absent it is skipped, so the release CI finishes with just the binary release and no red failure.
9.3 Do the build assumptions match the repository structure
The pipeline runs on the following three assumptions, and all three were confirmed to match the current repository structure by reproducing the binaries job locally.
- Frontend embed. The binaries job receives the
web-dist(the contents ofweb/out) uploaded by the frontend job intoserver/webui/dist, and//go:embed all:webui/distinserver/webui_embed.goembeds that location into the binary. The binaries job therefore does not rebuild the frontend; it callsbuild.shwithARTEX_SKIP_FRONTEND=1. - Binary and package paths.
build.sh --target <os>/<arch>produces thedist/artex-<os>-<arch>/artexbinary and a zip package underdist/. The zip contains the binary together with a start script (start.shon Linux/macOS,start.baton Windows),skills/,config.example.json, andREADME.md. - Copying the binary into the Docker image. The binaries job uploads the linux binary separately as
the
bin-linux-<arch>artifact, and the docker job receives it asdist/<arch>/artex. TheCOPY dist/${TARGETARCH}/artexinDockerfilepicks up that path via theTARGETARCHthat buildx fills in per platform during a multi-architecture build..dockerignoredoes not excludedist/, so the binary is included in the build context.
9.4 Still a pending decision: the Docker image namespace
The docker job currently leaves the image name as upstream's autumn27/artex, and which namespace this
fork should publish under is a separate decision (work/DECISIONS-FOR-JIWOO.md, item 8). Until that is
decided, the Docker Hub secrets are not set, and in the meantime a release publishes only the binary
zips and the checksum (the docker job is skipped).
9.5 Verifying locally without a tag
To check only the pipeline assumptions without cutting a public release, reproduce the binaries job locally. If you do not have Go locally, you can run the same thing under Docker.
# 1) Static frontend export (corresponds to the frontend job in release.yml)
cd web && npm ci && npm run build:static && cd ..
# 2) Place it where the binaries job receives the artifact
rm -rf server/webui/dist && mkdir -p server/webui/dist && cp -a web/out/. server/webui/dist/
# 3) Build one target with the same environment as the binaries job
docker run --rm -v "$PWD":/app -w /app \
-e ARTEX_SKIP_FRONTEND=1 -e ARTEX_SKIP_NPM_CI=1 \
-e ARTEX_COMPRESS=0 -e ARTEX_PACKAGE=1 -e ARTEX_PACKAGE_DIR=dist \
-e ARTEX_BUILD_VERSION=v0.0.0-local \
golang:1.26 bash -c 'apt-get update && apt-get install -y zip && ./build.sh --target linux/amd64'
# 4) Confirm the outputs: dist/artex-linux-amd64/artex · dist/*.zip · dist/SHA256SUMS
dist/artex-linux-amd64/artex is a statically linked ELF, and given -h it prints usage and exits
with code 0. This is the behavior the binaries job's smoke test confirms. Build outputs
(dist/, server/webui/dist/) are not committed to the repository (they are excluded by
.gitignore).