@caddis/cli 0.4.24 → 0.4.26
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.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/_registry.md +1 -0
- package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/AdoptCommon.ps1 +20 -2
- package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Get-AppInventory.ps1 +14 -4
- package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/New-AdoptionPlan.ps1 +2 -4
- package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Test-AppConformance.ps1 +4 -2
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +0 -1
- package/bundles/codex/.codex/skills/devops/adopt-app/SKILL.md +174 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/_template/profile.json +55 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/batch-job/profile.json +68 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/dotnet-service/profile.json +67 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/java-service/profile.json +62 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/node-service/profile.json +66 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/php-db/profile.json +75 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/python-web/profile.json +73 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/profiles/static-site/profile.json +87 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/references/config.example.json +18 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/references/profile-contract.md +207 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/references/standard.md +304 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/AdoptCommon.ps1 +830 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Get-AppInventory.ps1 +246 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/New-AdoptionPlan.ps1 +309 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Resolve-AppProfile.ps1 +246 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/scripts/Test-AppConformance.ps1 +345 -0
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/SKILL.md +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/_template/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/batch-job/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/dotnet-service/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/java-service/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/node-service/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/php-db/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/python-web/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/profiles/static-site/profile.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/config.example.json +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/profile-contract.md +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/references/standard.md +0 -0
- /package/bundles/{antigravity-plugin-extras → antigravity-plugin}/skills/adopt-app/scripts/Resolve-AppProfile.ps1 +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-service",
|
|
3
|
+
"status": "draft",
|
|
4
|
+
"description": "A Node.js/TypeScript API (for example Express, Fastify, Hono or NestJS), optionally with a built web frontend, often organised as an npm workspace.",
|
|
5
|
+
"detect": {
|
|
6
|
+
"allFiles": ["package.json"],
|
|
7
|
+
"fileContains": [
|
|
8
|
+
{
|
|
9
|
+
"file": "package.json",
|
|
10
|
+
"pattern": "\"(express|fastify|hono|@nestjs/core|koa)\""
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"runtimes": [
|
|
15
|
+
{
|
|
16
|
+
"kind": "node",
|
|
17
|
+
"version": "<from package.json engines, or an .nvmrc>",
|
|
18
|
+
"provision": "<installed on dev and prod hosts; confirm the installed version actually matches what's declared>"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"build": [
|
|
22
|
+
{
|
|
23
|
+
"what": "Dependencies",
|
|
24
|
+
"command": "npm ci (at the workspace root)"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"what": "Compile / bundle",
|
|
28
|
+
"command": "npm run build (in each workspace package that has one)"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"services": [
|
|
32
|
+
{
|
|
33
|
+
"role": "api",
|
|
34
|
+
"port": "<the app's HTTP port>",
|
|
35
|
+
"start": "<node <built entry point>, e.g. dist/server.js>",
|
|
36
|
+
"manager": "<a service manager or a container runtime>",
|
|
37
|
+
"bind": "<loopback-only, unless a trusted edge forwards identity>",
|
|
38
|
+
"health": "<the app's real health route - confirm it, do not assume a default path>"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"routing": {
|
|
42
|
+
"mode": "reverse-proxy",
|
|
43
|
+
"staticRoot": "<built web frontend output folder, if any>",
|
|
44
|
+
"apiPrefix": "/api"
|
|
45
|
+
},
|
|
46
|
+
"config": {
|
|
47
|
+
"files": ["<the env file the API reads, per environment>"],
|
|
48
|
+
"rule": "kept out of version control; a committed example file with placeholder values"
|
|
49
|
+
},
|
|
50
|
+
"data": {
|
|
51
|
+
"migrations": "<the app's migration tool, if any, e.g. Prisma or Knex>",
|
|
52
|
+
"state": "<where persistent data actually lives>"
|
|
53
|
+
},
|
|
54
|
+
"ci": {
|
|
55
|
+
"workflow": "<CI workflow file: lint (eslint), typecheck (tsc), test, build>",
|
|
56
|
+
"requiredJobs": ["<lint_and_test>"],
|
|
57
|
+
"deployJob": "<deploy_prod>"
|
|
58
|
+
},
|
|
59
|
+
"deploy": {
|
|
60
|
+
"script": "<deploy script: build in a staging copy, stop, swap, start>",
|
|
61
|
+
"verify": [
|
|
62
|
+
"the health route returns a body with a status field, not just HTTP 200",
|
|
63
|
+
"a rendered page changed, so an old build behind a redeployed API cannot pass"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "php-db",
|
|
3
|
+
"status": "draft",
|
|
4
|
+
"description": "A PHP application or packaged product with its own relational database (for example MariaDB or MySQL), served through a web server's PHP handler.",
|
|
5
|
+
"detect": {
|
|
6
|
+
"anyFiles": ["composer.json", "index.php"]
|
|
7
|
+
},
|
|
8
|
+
"runtimes": [
|
|
9
|
+
{
|
|
10
|
+
"kind": "php",
|
|
11
|
+
"version": "<exact version the app needs>",
|
|
12
|
+
"provision": "<installed on dev and prod hosts; never committed to the repo>"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"kind": "database",
|
|
16
|
+
"version": "<exact engine and version, e.g. MariaDB 10.x>",
|
|
17
|
+
"provision": "<installed separately; its data directory is state, restored from backup, never committed>"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"build": [
|
|
21
|
+
{
|
|
22
|
+
"what": "PHP dependencies",
|
|
23
|
+
"command": "composer install --no-dev (only if the app uses composer)"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"what": "Application customisation",
|
|
27
|
+
"command": "<copy the app's own extensions or config templates into place>"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"services": [
|
|
31
|
+
{
|
|
32
|
+
"role": "database",
|
|
33
|
+
"port": "<the database's port; keep it out of the web app's own port range>",
|
|
34
|
+
"start": "<the database server as a managed service, with its data directory outside the repo>",
|
|
35
|
+
"manager": "<a service manager>",
|
|
36
|
+
"bind": "<loopback-only, unless the app tier runs on a different host>",
|
|
37
|
+
"health": "<a database connectivity check>"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"role": "web",
|
|
41
|
+
"port": "<the app's HTTP port>",
|
|
42
|
+
"start": "<PHP under the web server's own PHP handler, or a PHP-served web process under a service manager>",
|
|
43
|
+
"manager": "<a service manager or the web server itself>",
|
|
44
|
+
"bind": "<loopback-only, unless a trusted edge forwards identity>",
|
|
45
|
+
"health": "<a page that exercises PHP and the database together>"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"routing": {
|
|
49
|
+
"mode": "<reverse-proxy|dedicated-site>",
|
|
50
|
+
"staticRoot": "<none - PHP serves its own pages>",
|
|
51
|
+
"apiPrefix": "<n/a>",
|
|
52
|
+
"note": "decide the login design before anything else: an in-app login sitting behind a shared authenticating edge can loop unless the app has its own routing prefix that bypasses that edge"
|
|
53
|
+
},
|
|
54
|
+
"config": {
|
|
55
|
+
"files": ["<the product's config file, e.g. a config-*.php or .env>"],
|
|
56
|
+
"rule": "holds database credentials: template it with placeholders, never commit the real one"
|
|
57
|
+
},
|
|
58
|
+
"data": {
|
|
59
|
+
"migrations": "<the product's own upgrade path, or hand-written SQL migrations>",
|
|
60
|
+
"state": "the database itself: a backup and restore plan is required before go-live"
|
|
61
|
+
},
|
|
62
|
+
"ci": {
|
|
63
|
+
"workflow": "<CI workflow file: PHP lint, the app's own tests, package the customisation>",
|
|
64
|
+
"requiredJobs": ["<to decide, based on what the app already tests>"],
|
|
65
|
+
"deployJob": "<deploy_prod>"
|
|
66
|
+
},
|
|
67
|
+
"deploy": {
|
|
68
|
+
"script": "<deploy script>",
|
|
69
|
+
"verify": [
|
|
70
|
+
"the database is reachable",
|
|
71
|
+
"a page that exercises PHP and the database returns real content, not a generic error page",
|
|
72
|
+
"the login flow reaches the app"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -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.
|