@caddis/cli 0.4.24 → 0.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bundles/antigravity-plugin/plugin.json +1 -1
  2. package/bundles/antigravity-plugin/skills/_registry.md +1 -0
  3. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  4. package/bundles/antigravity-plugin-extras/skills/_registry.md +0 -1
  5. package/bundles/codex/.codex/skills/devops/adopt-app/SKILL.md +174 -0
  6. package/bundles/codex/.codex/skills/devops/adopt-app/profiles/_template/profile.json +55 -0
  7. package/bundles/codex/.codex/skills/devops/adopt-app/profiles/batch-job/profile.json +68 -0
  8. package/bundles/codex/.codex/skills/devops/adopt-app/profiles/dotnet-service/profile.json +67 -0
  9. package/bundles/codex/.codex/skills/devops/adopt-app/profiles/java-service/profile.json +62 -0
  10. package/bundles/codex/.codex/skills/devops/adopt-app/profiles/node-service/profile.json +66 -0
  11. package/bundles/codex/.codex/skills/devops/adopt-app/profiles/php-db/profile.json +75 -0
  12. package/bundles/codex/.codex/skills/devops/adopt-app/profiles/python-web/profile.json +73 -0
  13. package/bundles/codex/.codex/skills/devops/adopt-app/profiles/static-site/profile.json +87 -0
  14. package/bundles/codex/.codex/skills/devops/adopt-app/references/config.example.json +18 -0
  15. package/bundles/codex/.codex/skills/devops/adopt-app/references/profile-contract.md +207 -0
  16. package/bundles/codex/.codex/skills/devops/adopt-app/references/standard.md +304 -0
  17. package/bundles/codex/.codex/skills/devops/adopt-app/scripts/AdoptCommon.ps1 +812 -0
  18. package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Get-AppInventory.ps1 +236 -0
  19. package/bundles/codex/.codex/skills/devops/adopt-app/scripts/New-AdoptionPlan.ps1 +311 -0
  20. package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Resolve-AppProfile.ps1 +246 -0
  21. package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Test-AppConformance.ps1 +343 -0
  22. package/bundles/manifest.json +3 -3
  23. package/package.json +1 -1
  24. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/SKILL.md +0 -0
  25. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/_template/profile.json +0 -0
  26. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/batch-job/profile.json +0 -0
  27. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/dotnet-service/profile.json +0 -0
  28. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/java-service/profile.json +0 -0
  29. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/node-service/profile.json +0 -0
  30. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/php-db/profile.json +0 -0
  31. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/python-web/profile.json +0 -0
  32. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/static-site/profile.json +0 -0
  33. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/config.example.json +0 -0
  34. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/profile-contract.md +0 -0
  35. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/standard.md +0 -0
  36. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/AdoptCommon.ps1 +0 -0
  37. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Get-AppInventory.ps1 +0 -0
  38. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/New-AdoptionPlan.ps1 +0 -0
  39. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Resolve-AppProfile.ps1 +0 -0
  40. /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Test-AppConformance.ps1 +0 -0
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "python-web",
3
+ "status": "draft",
4
+ "description": "A Python web application (for example FastAPI, Django or Flask) that serves an API and, often, a built single-page-app frontend from one process.",
5
+ "detect": {
6
+ "anyFiles": ["pyproject.toml", "requirements.txt"],
7
+ "fileContains": [
8
+ {
9
+ "file": ["pyproject.toml", "requirements.txt"],
10
+ "pattern": "(?i)\\b(fastapi|django|flask|starlette|aiohttp)\\b"
11
+ }
12
+ ],
13
+ "noneFiles": ["composer.json"]
14
+ },
15
+ "runtimes": [
16
+ {
17
+ "kind": "python",
18
+ "version": "<from pyproject.toml's requires-python, or a pinned version file>",
19
+ "provision": "<installed on dev and prod hosts; a per-app virtual environment>"
20
+ },
21
+ {
22
+ "kind": "node",
23
+ "version": "<from the frontend's package.json engines field, if a frontend is built>",
24
+ "provision": "<installed on hosts that build the frontend; not needed at runtime>"
25
+ }
26
+ ],
27
+ "build": [
28
+ {
29
+ "what": "Python dependencies",
30
+ "command": "pip install -e .[dev] (or pip install -r requirements.txt)"
31
+ },
32
+ {
33
+ "what": "Frontend bundle, if the app serves one",
34
+ "command": "npm ci && npm run build",
35
+ "cwd": "<the frontend folder, e.g. frontend>"
36
+ }
37
+ ],
38
+ "services": [
39
+ {
40
+ "role": "api",
41
+ "port": "<the app's HTTP port>",
42
+ "start": "<e.g. a uvicorn/gunicorn command, or the framework's own server run behind a real process manager>",
43
+ "manager": "<a service manager or a container runtime>",
44
+ "bind": "<loopback-only, unless a trusted edge forwards identity>",
45
+ "health": "<a real health endpoint, e.g. /health>"
46
+ }
47
+ ],
48
+ "routing": {
49
+ "mode": "reverse-proxy",
50
+ "staticRoot": "<built frontend output folder, if any>",
51
+ "apiPrefix": "/api"
52
+ },
53
+ "config": {
54
+ "files": ["<a per-environment env file, e.g. .env.<env>>"],
55
+ "rule": "kept out of version control; seeded from a committed example file; real values set by hand per environment"
56
+ },
57
+ "data": {
58
+ "migrations": "<the app's migration tool, e.g. alembic or Django migrations>",
59
+ "state": "<an external database; nothing durable lives on the app host itself>"
60
+ },
61
+ "ci": {
62
+ "workflow": "<CI workflow file, e.g. .github/workflows/ci.yml>",
63
+ "requiredJobs": ["<lint_and_test>", "<frontend_checks, if a frontend is built>"],
64
+ "deployJob": "<deploy_prod>"
65
+ },
66
+ "deploy": {
67
+ "script": "<deploy script: build first, swap second>",
68
+ "verify": [
69
+ "the health endpoint returns a body with a status field, not just HTTP 200",
70
+ "a rendered page or the frontend bundle's hash changed, so an old bundle behind a new backend cannot pass"
71
+ ]
72
+ }
73
+ }
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "static-site",
3
+ "status": "draft",
4
+ "description": "A static site or a statically-generated site (for example built with a static-site generator, or plain HTML/CSS/JS) with no backend process of its own.",
5
+ "detect": {
6
+ "anyFiles": [
7
+ "index.html",
8
+ "astro.config.mjs",
9
+ "astro.config.ts",
10
+ "gatsby-config.js",
11
+ "next.config.js",
12
+ "hugo.toml",
13
+ "hugo.yaml",
14
+ "_config.yml",
15
+ "mkdocs.yml",
16
+ "docusaurus.config.js",
17
+ "vuepress.config.js"
18
+ ],
19
+ "noneFiles": [
20
+ "pyproject.toml",
21
+ "requirements.txt",
22
+ "composer.json",
23
+ "index.php",
24
+ "**/*.csproj",
25
+ "pom.xml",
26
+ "build.gradle",
27
+ "build.gradle.kts"
28
+ ],
29
+ "noneContain": [
30
+ {
31
+ "file": "package.json",
32
+ "pattern": "\"(express|fastify|hono|@nestjs/core|koa)\""
33
+ }
34
+ ]
35
+ },
36
+ "runtimes": [
37
+ {
38
+ "kind": "node",
39
+ "version": "<from package.json engines, if a generator is used to build the site>",
40
+ "provision": "<needed only at build time, not to serve the finished site>"
41
+ }
42
+ ],
43
+ "build": [
44
+ {
45
+ "what": "Dependencies, if the site is generated",
46
+ "command": "npm ci"
47
+ },
48
+ {
49
+ "what": "Generate the static output",
50
+ "command": "npm run build"
51
+ }
52
+ ],
53
+ "services": [
54
+ {
55
+ "role": "web",
56
+ "port": "<the site's HTTP port, if served by a small process rather than a CDN or web server>",
57
+ "start": "<a static file server, or the site is served directly by a web server / CDN with no process of its own>",
58
+ "manager": "<a service manager, or none if a web server serves the files directly>",
59
+ "bind": "<loopback-only, if a trusted edge fronts it>",
60
+ "health": "<the home page returns 200 with the expected content>"
61
+ }
62
+ ],
63
+ "routing": {
64
+ "mode": "<dedicated-site|none>",
65
+ "staticRoot": "<the built output folder, e.g. dist or public>",
66
+ "apiPrefix": "<n/a>"
67
+ },
68
+ "config": {
69
+ "files": ["<build-time environment variables, if any>"],
70
+ "rule": "no secrets belong in a static bundle - anything shipped to the browser is public"
71
+ },
72
+ "data": {
73
+ "migrations": "none",
74
+ "state": "none - the site is a build artefact; content lives in the source repo or a content system, not on the serving host"
75
+ },
76
+ "ci": {
77
+ "workflow": "<CI workflow file: lint, build>",
78
+ "requiredJobs": ["<build>"],
79
+ "deployJob": "<deploy_prod>"
80
+ },
81
+ "deploy": {
82
+ "script": "<deploy script: build, then publish the output folder to where it is served from>",
83
+ "verify": [
84
+ "a rendered page's content or the build output's hash changed - not just an HTTP 200 from the serving host"
85
+ ]
86
+ }
87
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "outDir": ".caddis/adopt",
3
+ "fileBudget": 20000,
4
+ "skipFolders": [],
5
+ "skipPattern": "(?i)[\\\\/](\\.git|node_modules|\\.venv|venv|__pycache__|\\.mypy_cache|\\.pytest_cache|\\.tox|dist|build|target|bin|obj|vendor|packages|\\.terraform|\\.gradle|\\.next|\\.nuxt|coverage)([\\\\/]|$)",
6
+ "expectedRemotePattern": "^https://github\\.com/example-org/",
7
+ "untrackedThresholdMB": 10,
8
+ "_comments": {
9
+ "_read_this_first": "Copy this to .caddis/adopt-app.json in the repo you run the skill from, or point $env:ADOPT_APP_CONFIG at a copy elsewhere, then edit the values. JSON has no comment syntax, so this _comments object exists only to explain the keys above. Get-AdoptConfig merges every top-level key of your file over the defaults; nothing reads a key named _comments.",
10
+ "_every_key_is_live": "Each key below is read by a named script. There are deliberately no others. An earlier version of this file also listed blockingItems and contentPolicy, which no script ever consulted - a project could set them, see no effect and never find out. Config that does nothing is a promise the code does not keep, so those keys were removed from the code and from here together.",
11
+ "outDir": "Where phase output goes, relative to the repo root: <outDir>/<name>/inventory.json and the rest. Read by Resolve-AdoptOutDir, and only when a phase script runs without its own -OutDir.",
12
+ "fileBudget": "How many files the scan will walk per top-level folder before it stops. Read by Get-AppInventory.ps1 when -FileBudget is not passed; the parameter always wins. Reaching the budget does not fail quietly: the folder's size becomes a MINIMUM, the scan is marked partial, and the script exits 1 unless you pass -AllowPartial.",
13
+ "skipFolders": "Top-level folder names never walked at all. Read by Get-AppInventory.ps1 when -SkipFolder is not passed. Use it for a tree you already know is bulk data, so the budget is spent where the answer is still open. A skipped folder is recorded as NOT SCANNED, never as empty, and it stops source-only, runtime-manifest and data-recovery from passing.",
14
+ "skipPattern": "Paths whose CONTENTS are not classified - dependency folders, build output, caches. They are still measured for size and still classified as excludable content, but their files are not read for credentials or indexed for stack detection. Read by Get-AppInventory.ps1 and passed to Invoke-AdoptScan.",
15
+ "expectedRemotePattern": "The regex that means 'the team's host'. Read by Test-AppConformance.ps1 for the remote-on-team-host item: a repository whose remote URL does not match is a GAP even though it has a remote, which is how a repository sitting on someone's personal account gets caught. Leave it empty and any remote passes, because the check will not guess.",
16
+ "untrackedThresholdMB": "A folder that is neither committed nor ignored and is at least this large is a finding. Read by Test-AppConformance.ps1 when -UntrackedThresholdMB is not passed. This is the state that turns one 'git add -A' into gigabytes of runtime data in the history for ever, and no filename pattern finds it - only git can."
17
+ }
18
+ }
@@ -0,0 +1,207 @@
1
+ # The profile contract
2
+
3
+ A profile describes one KIND of component, not one app. It lives at
4
+ `profiles/<stack-name>/profile.json`. `profiles/_template/profile.json` is the starting point
5
+ for a new one, and phase 2 (`Resolve-AppProfile.ps1`) never matches `_template` itself against a
6
+ real inventory unless something explicitly asks `Get-AdoptProfiles` for it with
7
+ `-IncludeTemplate`.
8
+
9
+ Every profile file is loaded and checked against this contract BEFORE any detection runs, by
10
+ `Test-AdoptProfileContract` in `AdoptCommon.ps1`. **One broken profile fails detection for every
11
+ app being resolved, not just the one that owns it** - `Resolve-AppProfile.ps1` refuses to run at
12
+ all while any profile under `profiles/` fails the checks below.
13
+
14
+ ## Required keys
15
+
16
+ All ten must be present. The contract only checks that the key EXISTS; it does not validate the
17
+ shape of what is inside it.
18
+
19
+ | Key | Meaning |
20
+ |---|---|
21
+ | `name` | The stack's short name, e.g. `node-service`. Matches the folder name by convention. |
22
+ | `status` | `proven`, `draft`, or `placeholder`. See below. |
23
+ | `description` | One sentence: what kind of component this is. |
24
+ | `detect` | The rules that decide whether a component folder is this stack. See below. |
25
+ | `runtimes` | The language/runtime(s) it needs: kind, exact version, and how that version gets onto a box. |
26
+ | `build` | The ordered build steps: what each one does and the command. |
27
+ | `routing` | How traffic reaches it: reverse proxy, a rewrite rule, a dedicated site, or none. |
28
+ | `config` | Which files carry per-environment settings, and the rule that keeps them out of version control. |
29
+ | `data` | The migration tool, if any, and where persistent state actually lives. |
30
+ | `ci` | The CI workflow file, the jobs that must pass, and which job deploys. |
31
+ | `deploy` | The deploy script, and at least one check that would FAIL on a broken deploy. |
32
+
33
+ **Plus one rule that is not a top-level key check:** the profile must also carry `services` or
34
+ `schedule` (or both).
35
+
36
+ - `services` describes a component that answers requests. Each entry names a role
37
+ (`api`/`web`/`worker`/`database`), a port, a start command, the process manager that keeps it
38
+ running, the interface it binds, and a health check.
39
+ - `schedule` describes a batch component instead: what triggers a run - a scheduler, a
40
+ file-arrival watcher, or a manual/CI trigger.
41
+
42
+ A component that is neither long-running nor scheduled does not fit this contract yet.
43
+
44
+ ## Status values
45
+
46
+ | Status | Licenses |
47
+ |---|---|
48
+ | `placeholder` | Only `_template`'s own state. Its `name` is still the literal `<stack-name>` placeholder text - which is exactly why the "no leftover placeholder" rule below is skipped for `placeholder` alone. |
49
+ | `draft` | A description a person can follow by hand. It can still be DETECTED and matched to a real component - phase 2 will report it, with its status shown as `draft` - but nothing has automated it end to end yet. Follow it manually, write down what you actually did, and let it earn `proven` later. |
50
+ | `proven` | Something has actually automated this stack end to end: build, deploy and verify have all been run for real against a live component, not only described. |
51
+
52
+ Two more rules `Test-AdoptProfileContract` enforces on `status`:
53
+
54
+ - It must be exactly one of the three values above. Anything else fails the contract for every
55
+ profile in the directory, not only this one.
56
+ - Unless `status` is `placeholder`, `name` must not contain `<` or `>`. This is what stops a copy
57
+ of `_template` being left half-filled-in and silently matching real apps with its placeholder
58
+ name still inside it.
59
+
60
+ ## The five `detect` rule kinds
61
+
62
+ Checked by `Test-AdoptDetect`, against two things gathered per component: a list of relative
63
+ file paths, and the text of a handful of small files. Which files make it into either list is
64
+ bounded - see "What `detect` can actually see" below.
65
+
66
+ | Kind | Shape | Meaning |
67
+ |---|---|---|
68
+ | `allFiles` | list of globs | Every glob must match at least one path, or the profile does not match at all. |
69
+ | `anyFiles` | list of globs | At least one glob must match at least one path, or the profile does not match at all. |
70
+ | `noneFiles` | list of globs | If ANY glob matches, the profile does not match at all. A pure veto - it never adds to the score. |
71
+ | `fileContains` | list of `{ "file": glob, "pattern": regex }` | Every entry's glob must resolve to a file whose text matches `pattern`, or the profile does not match. |
72
+ | `noneContain` | list of `{ "file": glob, "pattern": regex }` | If any entry's glob resolves to a file whose text matches `pattern`, the profile does not match. A pure veto. |
73
+
74
+ All five are optional; omit whichever a stack does not need. A profile that declares none of them
75
+ never matches anything.
76
+
77
+ ## Glob-to-regex translation, exactly
78
+
79
+ Every glob - in `allFiles`, `anyFiles`, `noneFiles`, or a `fileContains`/`noneContain` entry's
80
+ `file` - goes through the same conversion, in this order:
81
+
82
+ 1. `[regex]::Escape($glob)` - escape every regex-special character literally, including `*`.
83
+ 2. Replace an escaped `**` (now `\*\*`) with `.*` - matches across any number of path segments.
84
+ 3. Replace a remaining escaped `*` (`\*`) with `[^\\/]*` - matches within ONE path segment only.
85
+ 4. Replace `/` with `[\\/]` - so a glob written with a forward slash matches a path recorded
86
+ with either separator.
87
+ 5. Anchor the whole thing as `(?i)^...$` - case-insensitive, and the WHOLE relative path must
88
+ match, not a substring of it.
89
+
90
+ So `src/*.ts` matches `src/index.ts` but not `src/api/index.ts` (one segment only), while
91
+ `src/**/*.ts` matches both. A bare filename like `package.json` matches only at the root of the
92
+ component being tested, not at any depth beneath it - write `**/package.json` for "anywhere
93
+ under this component."
94
+
95
+ ## How the score is computed
96
+
97
+ `Test-AdoptDetect` returns `0` for no match, or a positive score where a higher number means a
98
+ more specific match:
99
+
100
+ - `allFiles` declared and satisfied -> **+1**. Declared and NOT satisfied -> return `0`
101
+ immediately, regardless of anything else.
102
+ - `anyFiles` declared and satisfied -> **+1**. Declared and NOT satisfied -> return `0`
103
+ immediately.
104
+ - `fileContains` declared and satisfied -> **+1**. Declared and NOT satisfied -> return `0`
105
+ immediately.
106
+ - `noneFiles` and `noneContain` never add to the score. They are vetoes only: if either is
107
+ violated, the rule returns `0` no matter what else matched.
108
+
109
+ The maximum score is `3` (`allFiles` + `anyFiles` + `fileContains` all declared and all
110
+ satisfied). Phase 2 runs every profile against every component and keeps the highest-scoring
111
+ match as the winner. Everything else that scored above `0` is recorded in `alsoMatches`, never
112
+ discarded - a component genuinely matching two stacks is a decision for a person, and picking one
113
+ silently would hide that decision.
114
+
115
+ ## What `detect` can actually see
116
+
117
+ A rule can only use evidence the inventory actually indexed. Two different limits apply,
118
+ depending on what kind of file a glob names.
119
+
120
+ - **Manifests, entry points, config files, CI files, docs, and test markers** - anything matching
121
+ one of the fixed name lists `Get-AppInventory.ps1` already recognises (`package.json`,
122
+ `pyproject.toml`, `Dockerfile`, `main.py`, `.env*`, a file under a `workflows` folder,
123
+ `README*`, `pytest.ini`, and the rest) - are indexed at ANY depth in the tree.
124
+ - **Everything else** - an arbitrary filename a `detect` rule names that is not one of those
125
+ fixed names, for example `index.html` as evidence of a front end - only lands in the general
126
+ shallow path index, and that index is depth-limited: paths at depth 2 or shallower from the
127
+ scanned root are always kept; paths at depth 3-4 are kept only while a single cap shared across
128
+ the WHOLE scan (`DetectPathCap`, default 20000 paths) still has room. Past depth 4, or once
129
+ that cap is spent, a `detect` rule naming such a file cannot see it - not because the file is
130
+ absent, but because nothing recorded that it exists.
131
+
132
+ `fileContains`/`noneContain` add a second limit on top of path indexing: the named file must
133
+ actually be fetched, which only happens for files under 256 KB. A rule that reads a large
134
+ generated file will never see its content.
135
+
136
+ **Practical rule of thumb:** name a real manifest or entry-point filename in
137
+ `allFiles`/`anyFiles` whenever the stack has one - that evidence survives at any depth. Reserve
138
+ arbitrary, non-manifest filenames for secondary evidence, and expect them to be missed on a
139
+ deeply nested component or a very large tree.
140
+
141
+ ## Worked example: adding a new stack
142
+
143
+ Say a fleet starts running a Rust service and no profile describes it yet. Phase 2 would report
144
+ "NO PROFILE MATCHED ANY FOLDER" and exit 2 for that component.
145
+
146
+ 1. Copy the template:
147
+
148
+ cp -r profiles/_template profiles/rust-service
149
+
150
+ 2. Fill in `profiles/rust-service/profile.json`. Start with `status: "draft"` - nothing has
151
+ automated this stack yet, only described it:
152
+
153
+ {
154
+ "name": "rust-service",
155
+ "status": "draft",
156
+ "description": "A Rust HTTP service built with Cargo, e.g. Actix Web or Axum.",
157
+ "detect": {
158
+ "allFiles": ["Cargo.toml"],
159
+ "fileContains": [
160
+ { "file": "Cargo.toml", "pattern": "(actix-web|axum|rocket)" }
161
+ ]
162
+ },
163
+ "runtimes": [
164
+ {
165
+ "kind": "rust",
166
+ "version": "<from Cargo.toml or rust-toolchain.toml>",
167
+ "provision": "<installed on dev and prod hosts; confirm the installed version matches what is declared>"
168
+ }
169
+ ],
170
+ "build": [
171
+ { "what": "Compile", "command": "cargo build --release" }
172
+ ],
173
+ "services": [
174
+ {
175
+ "role": "api",
176
+ "port": "<the app's HTTP port>",
177
+ "start": "<the built binary, e.g. target/release/app>",
178
+ "manager": "<a service manager or a container runtime>",
179
+ "bind": "<loopback-only, unless a trusted edge forwards identity>",
180
+ "health": "<the app's real health route - confirm it, do not assume a default path>"
181
+ }
182
+ ],
183
+ "routing": { "mode": "reverse-proxy", "apiPrefix": "/api" },
184
+ "config": {
185
+ "files": ["<the env file it reads, per environment>"],
186
+ "rule": "kept out of version control; a committed example file with placeholder values"
187
+ },
188
+ "data": {
189
+ "migrations": "<its migration tool, if any>",
190
+ "state": "<where persistent state actually lives>"
191
+ },
192
+ "ci": {
193
+ "workflow": "<CI workflow file>",
194
+ "requiredJobs": ["<build_and_test>"],
195
+ "deployJob": "<deploy_prod>"
196
+ },
197
+ "deploy": {
198
+ "script": "<deploy script>",
199
+ "verify": ["<a check that would FAIL on a broken deploy>"]
200
+ }
201
+ }
202
+
203
+ 3. Re-run phase 2. If a real component's `Cargo.toml` mentions `axum`, it now scores `2`
204
+ (`allFiles` + `fileContains`) and is reported with `status: draft`.
205
+ 4. Follow the profile by hand on a real adoption. Write down what was actually done for build,
206
+ deploy and health, then raise `status` to `proven` only once all of it has genuinely been
207
+ automated - not before.