docs: PRD + playbook REA + laporan modul 1 (composerize)

This commit is contained in:
oxpoe committed 2026-10-15 06:47:55 +07:00
commit ca45f67b0c
6 files changed
+2248

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
# REA — Reverse Engineering & Analysis (Playbook)
> Skill kerja untuk menganalisis **logika backend** proyek referensi, lalu mem-porting perilakunya
> ke Go di dalam `tia`. Definisi lengkap: `docs/PRD.md` §12.
## Aturan (wajib, tidak bisa dinegosiasi)
1. **Cek lisensi dulu** — langsung dari repo sumber, sebelum membaca kode serius. Hanya proyek
open source (MIT/Apache/BSD dsb). Jangan pernah menganalisis proyek closed-source.
2. **Tulis ulang, jangan salin.** Pelajari perilaku → implementasi sendiri dalam Go.
Tidak ada copy-paste verbatim.
3. Jika terpaksa dekat dengan aslinya (mis. tabel pemetaan flag): catat di laporan sebagai
"diadaptasi", sertakan atribusi di `THIRD_PARTY_NOTICES.md`.
4. Jangan ambil nama, logo, aset visual, atau branding proyek referensi.
5. Repo referensi di-clone ke `reference/` (git-ignored). **Tidak pernah** ikut ter-commit.
## Alur kerja
```
1 Scope → 2 Map → 3 Trace → 4 Spec → 5 Port → 6 Verify
↑ │
└── beda perilaku ┘
```
| Langkah | Aktivitas | Output |
|---|---|---|
| 1. **Scope** | Modul & batasnya, verifikasi lisensi | Catatan scope + lisensi di laporan |
| 2. **Map** | Petakan file/fungsi/struktur data relevan | Peta modul |
| 3. **Trace** | Telusuri alur input → output, edge case & error path | Diagram alur + daftar edge case |
| 4. **Spec** | Spesifikasi netral: input, output, aturan, tabel pemetaan, perintah Docker | `docs/rea/<modul>.md` + test vectors |
| 5. **Port** | Implementasi ulang dalam Go (di `internal/core`, tanpa UI) | Kode Go + unit test |
| 6. **Verify** | Uji paritas: input sama → output setara (golden/differential test) | Laporan paritas (lolos/gagal per kasus) |
## Definition of Done per modul
- [ ] Lisensi sumber diverifikasi & dicatat (repo, commit, lisensi)
- [ ] Laporan `docs/rea/<modul>.md` selesai (pakai `docs/rea/TEMPLATE.md`)
- [ ] Test vectors tersedia
- [ ] Implementasi Go terpisah dari UI (dipakai TUI **dan** CLI)
- [ ] Uji paritas lolos sesuai target (≥ 95% flag umum untuk converter)
- [ ] `THIRD_PARTY_NOTICES.md` diperbarui bila perlu
## Indeks modul (PRD §12.6)
| # | Modul | Repo | Fase | Status |
|---|---|---|---|---|
| 1 | Parser `docker run` → compose | `composerize/composerize` | 3 | ✅ [composerize.md](composerize.md) |
| 2 | Scan stack & rekonsiliasi status | `louislam/dockge` | 3 | ⬜ belum |
| 3 | Arsitektur primary ↔ agent (ide multi-host) | `louislam/dockge` | 6 | ⬜ belum |
| 4 | Pola pemanggilan Docker SDK | `jesseduffield/lazydocker` | 2, 4 | ⬜ belum |
| 5 | Manajemen stream/task background | `jesseduffield/lazydocker` | 2 | ⬜ belum |
| 6 | Custom commands (template placeholder) | `jesseduffield/lazydocker` | 5 | ⬜ belum |
| 7 | Format output `docker compose ls/ps/config` | `docker/compose` (CLI lokal) | 3 | ⬜ belum |
Repos referensi (commit yang dianalisis):
| Repo | Commit | Lisensi |
|---|---|---|
| composerize | `3e39eb7` | MIT |
| dockge | `f809ae1` | MIT |
| lazydocker | `7e7aadc` | MIT |
## Cara memanggil modul baru
Prompt singkat: `REA: analisis modul <nama> dari <repo>` — agent mengikuti alur 1–6 di atas,
selalu mulai dari verifikasi lisensi dan membaca `docs/rea/TEMPLATE.md`.
+40
View File
@@ -0,0 +1,40 @@
# REA — <Nama Modul>
> Status: DRAFT | SPEC FINAL | PORTED | VERIFIED
> Fase: <fase PRD> · Target kode: `<path Go>`
## 1. Ringkasan & Tujuan
<Apa modul ini, kenapa dianalisis, apa yang akan dihasilkan di tia.>
## 2. Sumber
| Item | Nilai |
|---|---|
| Repo | `<url>` |
| Commit | `<sha>` |
| Lisensi | `<SPDX>` (diverifikasi dari file LICENSE, tanggal) |
| Tanggal analisis | `<yyyy-mm-dd>` |
## 3. Alur Eksekusi
<Diagram mermaid + langkah bernomor dari input ke output.>
## 4. Struktur Data & Format I/O
<Bentuk input, struktur internal, bentuk output.>
## 5. Aturan & Edge Case
<Tabel atau daftar: aturan normalisasi, kasus aneh, error path, bug referensi yang TIDAK ditiru.>
## 6. Perintah Docker/Compose yang Dipanggil
| Perintah | Kapan | Format output yang diharapkan |
|---|---|---|
## 7. Test Vectors
<Sekumpulan pasangan input → output yang diharapkan. Ditulis di sini; nanti dikonsumsi golden test.>
## 8. Perbedaan yang Disengaja dari Referensi
<Setiap perbedaan + alasannya. Wajib untuk setiap deviasi perilaku.>
## 9. Rencana Porting Go
<File target, struktur fungsi, dependensi, keputusan desain.>
## 10. Verifikasi Paritas
<Metode (golden/differential), korpus, target %, status.>
+578
View File
@@ -0,0 +1,578 @@
# REA — Modul 1: Parser `docker run` → Compose (composerize)
> Status: SPEC FINAL · Fase 3 · Target kode: `internal/stack/converter.go`
> Analisis: 2026-10-15 · Metode: baca sumber + fixture test (bukan black-box)
## 1. Ringkasan & Tujuan
`composerize` mengubah perintah `docker run` menjadi file compose YAML. Di tia, logika ini menjadi
**konverter inti** yang dipakai dua-duanya: modal konverter TUI (F-008) dan `tia convert` CLI (F-028).
Yang diporting: **pipeline parsing, tabel pemetaan flag, dan aturan normalisasi**.
Yang tidak diport: fitur merge dengan file compose existing via stdin (ditangani layer stack tia
dengan cara berbeda), website, dan UI mereka.
## 2. Sumber
| Item | Nilai |
|---|---|
| Repo | https://github.com/composerize/composerize |
| Commit | `3e39eb7` (clone `--depth 1`, 2026-10-15) |
| Lisensi | **MIT** — Copyright (c) 2024 Composerize (diverifikasi dari file `LICENSE`) |
| File kunci | `packages/composerize/src/{index,logic,mappings}.js`, `cli.js`, `__tests__/*.js` |
| Dependency eksternal | `yargs-parser` (tokenizer), `composeverter` (yamlParse/yamlStringify/migrasi format), `deepmerge` |
Catatan: `composeverter` adalah paket npm terpisah (tidak ada di repo). Migrasi format
(`version: '3'` → common spec) terjadi di sana. Untuk tia kita langsung memproduksi output
common-spec, jadi bagian itu tidak perlu diport.
## 3. Alur Eksekusi
```
input (string perintah)
│
├─ 1. split multi-command: /^(?:\s*\$)?\s*(?:docker|podman)\s+/gm
│ → setiap segmen harus match ^(run|create|container run|service create),
│ kalau bukan → komentar "# ignored : docker/podman <cmd>"
│
├─ 2. normalisasi teks (urutan PENTING):
│ a. hapus komentar baris: /\n\s*#[^\n]+/g
│ b. lindungi $(...) : ganti spasi dalam $() dengan '¤'
│ c. hapus line continuation: /\\\n/g (backslash + newline)
│ d. collapse whitespace: /(\s)+/g → ' '
│ e. trim
│ f. perbaiki '-p80' → '-p 80': /\s-p(\d)/g
│ g. '\ \' (spasi-backslash-spasi) → ' ' [utf8: '\' literal]
│ h. hapus ';' di akhir: /\s*;$/g
│
├─ 3. buang prefix perintah:
│ /^(?:\s*\$\s+)?(?:docker|podman)\s+(run|create|container\s+run|service\s+create)/
│
├─ 4. tokenisasi via yargs-parser:
│ configuration: halt-at-non-option=true, camel-case-expansion=false,
│ boolean-negation=false
│ boolean flags: i interactive t tty d detach rm init help privileged
│ P publish-all no-healthcheck oom-kill-disable
│ read-only sig-proxy
│ → halt-at-non-option: semua token setelah IMAGE menjadi argumen command
│ → boolean-negation=false: '--no-healthcheck' tetap key 'no-healthcheck'
│
├─ 5. bersihkan nilai: stripQuotes() + kembalikan '¤' → spasi (dalam $())
│
├─ 6. loop setiap key → MAPPINGS[key] → ComposeEntry → deepmerge ke service
│ key tak dikenal → baris komentar "# --key=value"
│
├─ 7. image = token positional pertama (stripQuotes)
│ token positional sisanya → command: gabung dengan spasi
│
├─ 8. post-processing:
│ a. network_mode == host → hapus ports
│ b. volume source tanpa '/', '\', '$', '<', '>' → dianggap named volume
│ → top-level volumes + {external: {name}} + komentar
│ c. networks: nilai map kosong → null; semua null → jadikan array nama
│ → top-level networks {external: true, name} + komentar
│
├─ 9. bungkus: {version:'3', services:{<nama>: service}} — digabung (deepmerge)
│ dengan existing compose bila ada
│
└─ 10. stringify YAML (indent default library 4, CLI 2)
+ prepend komentar (ignored options, external network/volume)
+ migrasi format: latest → common spec (hapus version, tambah
"name: <your project name>"), v2x → "2.4", v3x → "3"
```
## 4. Struktur Data & Format I/O
**Input:** string (satu atau banyak perintah `docker/podman run/create/...`).
**Output:** string YAML, atau `SyntaxError` bila tidak ada satu pun perintah valid:
```
must have at least a valid docker/podman run/create/service create/container run command
```
**Tipe nilai flag** (`mappings.js`):
| Type | Arti |
|---|---|
| `Array` | list of string |
| `Value` | string apa adanya |
| `IntValue` / `FloatValue` | `parseInt` / `parseFloat` |
| `Switch` | boolean (`value === 'true'` \|\| `true`) |
| `Envs` | `["K=V", "K"]` — split di `=` pertama, sisanya digabung lagi |
| `Networks` | `host/bridge/none/container:*` → `network_mode`; selain itu → `networks.{nama}` |
| `Map` | nilai tunggal `k=v,k2=v2` → object (nilai numerik → int) |
| `MapArray` | seperti Map tapi tiap kemunculan flag jadi satu object (dipakai `--mount`) |
| `Ulimits` | `type=soft[:hard]` → `ulimits.<type>: int \| {soft,hard}` |
| `Gpus` | `all` \| angka → `deploy.resources.reservations.devices[{driver:nvidia,count,capabilities:[gpu]}]` |
| `DeviceBlockIOConfigWeight` | `path:weight` → `[{path, weight:int}]` |
| `DeviceBlockIOConfigRate` | `path:rate` → `[{path, rate:int\|string}]` |
**Nama service:** dari image → buang registry (`split('/')` terakhir) → buang tag
(`split(':')[0]`). Image kosong → `!!!invalid!!!`.
## 5. Aturan & Edge Case
| # | Kasus | Perilaku referensi | Catatan untuk tia |
|---|---|---|---|
| 1 | Komentar di input | Hanya dihapus jika didahului newline (`/\n\s*#/`) — komentar di baris pertama TIDAK terhapus | Tiru regex persis; uji kasus baris pertama |
| 2 | `$(pwd)`, `$(id -u)` | Spasi di dalam `$()` dilindungi `¤` agar tokenizer tidak memecah | Harus ditiru — krusial, banyak dipakai di compose nyata |
| 3 | Line continuation `\` + newline | Dihapus lalu whitespace di-collapse | Sama |
| 4 | `-p80:80` | Diperbaiki jadi `-p 80:80` (hanya untuk `-p` + digit!) | Sama. `-eFOO=bar` **tidak** diperbaiki — perlu uji differential |
| 5 | `;` di akhir | Dihapus | Sama |
| 6 | `-d` / `--detached` | Diabaikan **diam-diam** (mapping ke path `''`) | Sama |
| 7 | `--detach` (panjang) | Muncul sebagai komentar `# --detach` (inkonsisten dgn `-d`) | tia: ignore senyap keduanya (lihat §8) |
| 8 | `--rm` | Diabaikan diam-diam | Sama |
| 9 | Flag tak dikenal | Komentar `# ignored options for '<service>'` + daftar flag di atas YAML | Sama (tanpa gagal) |
| 10 | `--network host` | `network_mode: host` + ports dihapus | Sama |
| 11 | `--network appnet` | `networks: [appnet]` + top-level `networks.appnet.external: true, name` + komentar peringatan | Sama (parity dgn docker run: network harus sudah ada) |
| 12 | Named volume `-v vol:/data` | `volumes.vol.external: {name: vol}` + komentar | **Berbeda di tia** (lihat §8) |
| 13 | `--ip x` tanpa `--network` | Network fallback = `"default"` → `networks.default.ipv4_address` | Tiru; hasilnya agak aneh tapi konsisten |
| 14 | `--tmpfs` berulang | Digabung salah jadi `"/a,/b"` (String(array)) | tia: list (lihat §8) |
| 15 | `--mount ... ,readonly` | Key tanpa nilai menghasilkan value `undefined` → hilang dari YAML | tia: `readonly: true` (lihat §8) |
| 16 | Command setelah image | Token positional digabung dengan spasi (kutip hilang) | Tiru; tandai untuk uji differential |
| 17 | Kutip pada nilai | `stripQuotes` hanya membuang kutip terluar yang sepasang | Sama |
| 18 | Nilai `-` tunggal / angka | `Map`/`MapArray` mengubah string numerik jadi int | Sama |
| 19 | `--volumes-from` | Dipetakan ke `volume_from` | **BUG referensi** — compose spec = `volumes_from`. tia pakai yang benar |
| 20 | Tanpa perintah valid | `SyntaxError` dengan pesan spesifik | Pesan i18n di layer UI; inti error setara |
## 6. Perintah Docker/Compose yang Dipanggil
Tidak ada — murni transformasi string→YAML. (Verifikasi output: `docker compose config` opsional
untuk validasi di luar converter.)
## 7. Tabel Pemetaan Flag (docker run → compose)
Diambil dari `mappings.js` + short-flag alias. `[A]`=Array, `[V]`=Value, `[I]`=Int, `[F]`=Float,
`[S]`=Switch, `[E]`=Envs, `[N]`=Networks, `[M]`=Map, `[MA]`=MapArray, `[U]`=Ulimits, `[G]`=Gpus.
| docker run | compose | tipe |
|---|---|---|
| `--add-host` | `extra_hosts` | A |
| `--blkio-weight` | `blkio_config.weight` | I |
| `--blkio-weight-device` | `blkio_config.weight_device` | DeviceBlockIO |
| `--cap-add` / `--cap-drop` | `cap_add` / `cap_drop` | A |
| `--cgroup-parent` | `cgroup_parent` | V |
| `--cgroupns` | `cgroup` | V |
| `--cpu-period` `--cpu-quota` `--cpu-rt-period` `--cpu-rt-runtime` | `cpu_period` `cpu_quota` `cpu_rt_period` `cpu_rt_runtime` | V (string!) |
| `-c` `--cpu-shares` | `cpu_shares` | I |
| `--cpus` | `deploy.resources.limits.cpus` | F |
| `--device` | `devices` | A |
| `--device-cgroup-rule` | `device_cgroup_rules` | A |
| `--device-read-bps` `--device-read-iops` `--device-write-bps` `--device-write-iops` | `blkio_config.device_*` | DeviceBlockIO |
| `--dns` `--dns-opt` `--dns-search` | `dns` `dns_opt` `dns_search` | A |
| `--domainname` | `domainname` | V |
| `--entrypoint` | `entrypoint` | A (satu elemen) |
| `--env-file` | `env_file` | A |
| `-e` `--env` | `environment` | E |
| `--expose` | `expose` | A |
| `--gpus` | `deploy.resources.reservations.devices` | G |
| `--group-add` | `group_add` | A |
| `--health-cmd` `--health-interval` `--health-retries` `--health-start-period` `--health-timeout` | `healthcheck.{test,interval,retries,start_period,timeout}` | V |
| `--no-healthcheck` | `healthcheck.disable` | S |
| `-h` `--hostname` | `hostname` | V |
| `--init` | `init` | S |
| `-i` `--interactive` | `stdin_open` | S |
| `--ip` `--ip6` | `networks.<net>.ipv4_address` / `.ipv6_address` | V |
| `--ipc` | `ipc` | V |
| `--isolation` | `isolation` | V |
| `-l` `--label` | `labels` | A |
| `--link` `--link-local-ip` | `links` / `networks.<net>.link_local_ips` | A |
| `--log-driver` | `logging.driver` | V |
| `--log-opt` | `logging.options` | M |
| `--mac-address` | `mac_address` | V |
| `-m` `--memory` | `deploy.resources.limits.memory` | V |
| `--memory-reservation` | `deploy.resources.reservations.memory` | V |
| `--memory-swap` / `--memory-swappiness` | `memswap_limit` / `mem_swappiness` | V |
| `--mount` | `volumes` (long syntax object) | MA |
| `--name` | `container_name` | V |
| `--net` `--network` | `networks` / `network_mode` | N |
| `--network-alias` | `networks.<net>.aliases` | A |
| `--oom-kill-disable` | `oom_kill_disable` | S |
| `--oom-score-adj` | `oom_score_adj` | V |
| `--pid` | `pid` | V |
| `--pids-limit` | `deploy.resources.limits.pids` | I |
| `--platform` | `platform` | V |
| `--privileged` | `privileged` | S |
| `-p` `--publish` | `ports` | A |
| `--pull` | `pull_policy` | V |
| `--read-only` | `read_only` | S |
| `--restart` | `restart` | V |
| `--runtime` | `runtime` | V |
| `--security-opt` | `security_opt` | A |
| `--shm-size` | `shm_size` | V |
| `--stop-signal` | `stop_signal` | V |
| `--stop-timeout` | `stop_grace_period` | V |
| `--storage-opt` | `storage_opt` | M |
| `--sysctl` | `sysctls` | A |
| `--tmpfs` | `tmpfs` | V |
| `-t` `--tty` | `tty` | S |
| `--ulimit` | `ulimits` | U |
| `-u` `--user` | `user` | V |
| `--userns` | `userns_mode` | V |
| `--uts` | `uts` | V |
| `-v` `--volume` | `volumes` | A |
| `--volumes-from` | `volumes_from` *(ref: `volume_from` — bug)* | A |
| `-w` `--workdir` | `working_dir` | V |
| `--rm` `-d` `--detached` | *(diabaikan)* | — |
**Tidak ditangani referensi** (tia boleh menambah nanti, catat sebagai ekstensi):
`--health-start-interval`, `--annotation`, `--cidfile`, `--label-file`, `--attach`,
`-P/--publish-all`, `--sig-proxy`, `--stop-grace-period` (bukan flag docker run; yang benar `--stop-timeout`).
## 8. Test Vectors
Format target tia: **common spec** (tanpa `version:`, tanpa `name:` — nama project diurus layer
stack), indent 2, urutan key = urutan flag lalu `image` terakhir (mengikuti referensi).
### V1 — basic
```
docker run -p 80:80 foobar/baz:latest
```
```yaml
services:
baz:
ports:
- 80:80
image: foobar/baz:latest
```
### V2 — flags umum
```
docker run -d --name web -p 8080:80 -v ./html:/usr/share/nginx/html:ro -e TZ=Asia/Jakarta --restart unless-stopped nginx:latest
```
```yaml
services:
web:
container_name: web
ports:
- 8080:80
volumes:
- ./html:/usr/share/nginx/html:ro
environment:
- TZ=Asia/Jakarta
restart: unless-stopped
image: nginx:latest
```
*(nama service = `nginx` karena diambil dari image; `--name` hanya mengisi `container_name`)*
### V3 — multi-line + `$(...)`
```
docker run \
-d \
--name tor-relay \
-e TZ=Europe/London \
-e PUID=$(id -u) \
-v $(pwd)/tor/data:/data:Z \
-p 9001:9001 \
--restart always \
ilshidur/tor-relay
```
```yaml
services:
tor-relay:
container_name: tor-relay
environment:
- TZ=Europe/London
- PUID=$(id -u)
volumes:
- $(pwd)/tor/data:/data:Z
ports:
- 9001:9001
restart: always
image: ilshidur/tor-relay
```
### V4 — kutip pada nilai
```
docker run --name="foobar" -e GREETING="hello world" nginx
```
```yaml
services:
nginx:
container_name: foobar
environment:
- GREETING=hello world
image: nginx
```
### V5 — `-p80` nempel
```
docker run -p8080:80 nginx
```
```yaml
services:
nginx:
ports:
- 8080:80
image: nginx
```
### V6 — flag tak dikenal
```
docker run -d -z machin --unknown-long=truc nginx
```
```yaml
# ignored options for 'nginx'
# -z=machin
# --unknown-long=truc
services:
nginx:
image: nginx
```
### V7 — network bernama (external)
```
docker run --network appnet nginx
```
```yaml
# named network 'appnet' is marked as "external" (used by service 'nginx'), so either remove "external" from network definition or it needs to be created using: docker network create -d bridge appnet
services:
nginx:
networks:
- appnet
image: nginx
networks:
appnet:
external: true
name: appnet
```
### V8 — network mode host (ports dibuang)
```
docker run --network host -p 8080:80 nginx
```
```yaml
services:
nginx:
network_mode: host
image: nginx
```
### V9 — named volume (perilaku tia; §8 perbedaan #3)
```
docker run -v vol:/data nginx
```
```yaml
services:
nginx:
volumes:
- vol:/data
image: nginx
volumes:
vol: {}
```
### V10 — `--mount` long syntax
```
docker run --mount type=bind,source=/host,target=/container,readonly nginx
```
```yaml
services:
nginx:
volumes:
- type: bind
source: /host
target: /container
readonly: true
image: nginx
```
### V11 — healthcheck
```
docker run --health-cmd "curl -f http://localhost/" --health-interval 30s --health-retries 3 --health-timeout 5s --health-start-period 10s nginx
```
```yaml
services:
nginx:
healthcheck:
test: curl -f http://localhost/
interval: 30s
retries: 3
timeout: 5s
start_period: 10s
image: nginx
```
### V12 — log opt (berulang → satu object)
```
docker run --log-opt max-size=1g --log-opt max-file=3 --restart always nginx
```
```yaml
services:
nginx:
logging:
options:
max-size: 1g
max-file: 3
restart: always
image: nginx
```
### V13 — limits
```
docker run --memory 512m --cpus 1.5 --pids-limit 200 nginx
```
```yaml
services:
nginx:
deploy:
resources:
limits:
memory: 512m
cpus: 1.5
pids: 200
image: nginx
```
### V14 — gpus
```
docker run --gpus all nvidia/cuda:12.0-base
```
```yaml
services:
cuda:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities:
- gpu
image: nvidia/cuda:12.0-base
```
### V15 — command setelah image
```
docker run nginx:alpine nginx -g daemon off;
```
```yaml
services:
nginx:
image: nginx:alpine
command: nginx -g daemon off
```
*(`;` akhir dihapus oleh normalisasi)*
### V16 — multi perintah + perintah diabaikan
```
docker run -p 80:80 foo/bar:latest
docker stop
docker run -p 80:80 foo/buzz:latest
```
```yaml
# ignored : docker/podman stop
services:
bar:
ports:
- 80:80
image: foo/bar:latest
buzz:
ports:
- 80:80
image: foo/buzz:latest
```
### V17 — error
```
foo bar
```
→ error (setara): `must have at least a valid docker/podman run/create/service create/container run command`
### V18 — env-file + env
```
docker run --env-file .env -e DEBUG=1 app/api:1.2
```
```yaml
services:
api:
env_file:
- .env
environment:
- DEBUG=1
image: app/api:1.2
```
### V19 — ulimit
```
docker run --ulimit nofile=1024:2048 --ulimit nproc=65535 nginx
```
```yaml
services:
nginx:
ulimits:
nofile:
soft: 1024
hard: 2048
nproc: 65535
image: nginx
```
### V20 — ip + network
```
docker run --network appnet --ip 10.0.0.5 nginx
```
```yaml
# named network 'appnet' is marked as "external" ...
services:
nginx:
networks:
appnet:
ipv4_address: 10.0.0.5
image: nginx
networks:
appnet:
external: true
name: appnet
```
> **Perlu verifikasi differential** (langsung lawan composerize asli, butuh Node):
> kutip di dalam `command`, `-eFOO=bar` (short flag nempel), `--` separator,
> `--privileged false`, path Windows (`C:\`), dan format kutip YAML-nya.
> Daftar ini masuk korpus uji paritas Fase 3.
## 9. Perbedaan yang Disengaja dari Referensi
| # | Perbedaan | Alasan |
|---|---|---|
| 1 | Output = **common spec** murni: tanpa `version:`, tanpa `name: <your project name>` | Tujuannya tia: file stack valid di Compose v2+; nama project ditentukan folder stack, bukan placeholder |
| 2 | Indent default **2** (referensi library 4, CLI 2) | Konvensi compose + samakan dengan perilaku CLI referensi |
| 3 | **Named volume tidak** di-mark `external` (`volumes: {vol: {}}`) | `docker run -v vol:/data` auto-create volume; `external` mengubah perilaku (harus sudah ada). Network tetap `external: true` karena `docker run --network X` memang menuntut X sudah ada |
| 4 | `--volumes-from` → `volumes_from` | `volume_from` adalah bug referensi (bukan field compose spec) |
| 5 | `--tmpfs` berulang → list `["/a", "/b"]` | Referensi menggabung jadi `"/a,/b"` (bug) |
| 6 | `--mount ...,readonly` → `readonly: true` | Referensi menjatuhkan key tanpa nilai |
| 7 | `-d` dan `--detach` sama-sama diabaikan senyap | Referensi inkonsisten (`-d` senyap, `--detach` komentar) |
| 8 | Tidak ada merge stdin; "tambah ke stack" ditangani layer stack (baca-tulis YAML compose) | Arsitektur tia: converter tetap murni string→string agar bisa dipakai TUI & CLI dengan hasil identik |
| 9 | Pesan error dibungkus i18n (F-023) | Prinsip tia; isi/arti error tetap sama |
## 10. Rencana Porting Go
File: `internal/stack/converter.go` (+ `converter_test.go`, `testdata/`).
```
Tokenizer (tulis sendiri, ~80 baris):
1. preprocess: komentar, $() → placeholder, line-continuation, collapse space
2. split token dgn dukungan kutip tunggal/ganda
3. klasifikasi: --opt=val | --opt val | -x val | -xval | -it (cluster) | positional
4. halt-at-non-option: setelah positional pertama → semua ke args command
Parser: map[string][]string (key → nilai; berulang jadi slice, urutan dipertahankan)
Mapper: tabel MAPPINGS di Go (key → type + path)
Builder: bangun pohon kompose secara TERURUT (yaml.Node / ordered map) → yaml.v3
```
Keputusan desain:
- **Urutan key deterministik** — map Go tidak berurutan; pakai `yaml.Node` (atau tipe ordered
map kecil) agar output stabil dan bisa dibandingkan di golden test.
- Tabel MAPPINGS ditulis **dari perilaku** (bukan copy kode JS); atribusi di `THIRD_PARTY_NOTICES.md`.
- Test vectors V1–V20 di atas dikonsumsi sebagai golden test. Differential test vs composerize
asli dijalankan di CI (`node`, npm `composerize`), YAML dinormalisasi (sort key) lalu dibandingkan;
target paritas ≥ 95% untuk flag umum — selisih yang tersisa wajib ada di tabel §9.
## 11. Verifikasi Paritas
| Item | Status |
|---|---|
| Test vectors tertulis | ✅ (V1–V20) |
| Golden test di Go | ⬜ Fase 3 |
| Differential vs composerize (Node) | ⬜ Fase 3 — perlu `node`/`npm` di CI |
| Laporan paritas | ⬜ Fase 3 |