@numa-tech/numa 1.13.2 → 1.13.4

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 (46) hide show
  1. package/README.md +75 -10
  2. package/dist/application-onboarding/client.d.ts +6 -1
  3. package/dist/application-onboarding/client.js +11 -1
  4. package/dist/application-onboarding/client.js.map +1 -1
  5. package/dist/application-onboarding/commands.d.ts +23 -1
  6. package/dist/application-onboarding/commands.js +278 -30
  7. package/dist/application-onboarding/commands.js.map +1 -1
  8. package/dist/application-onboarding/schemas.d.ts +109 -0
  9. package/dist/application-onboarding/schemas.js +17 -4
  10. package/dist/application-onboarding/schemas.js.map +1 -1
  11. package/dist/command-catalog.js +134 -13
  12. package/dist/command-catalog.js.map +1 -1
  13. package/dist/gitops/client.d.ts +41 -0
  14. package/dist/gitops/client.js +13 -1
  15. package/dist/gitops/client.js.map +1 -1
  16. package/dist/gitops/commands.d.ts +24 -1
  17. package/dist/gitops/commands.js +64 -4
  18. package/dist/gitops/commands.js.map +1 -1
  19. package/dist/gitops/schemas.d.ts +50 -0
  20. package/dist/gitops/schemas.js +37 -0
  21. package/dist/gitops/schemas.js.map +1 -1
  22. package/dist/jenkins-gitops-rollouts/client.d.ts +197 -0
  23. package/dist/jenkins-gitops-rollouts/client.js +126 -0
  24. package/dist/jenkins-gitops-rollouts/client.js.map +1 -0
  25. package/dist/jenkins-gitops-rollouts/commands.d.ts +50 -0
  26. package/dist/jenkins-gitops-rollouts/commands.js +358 -0
  27. package/dist/jenkins-gitops-rollouts/commands.js.map +1 -0
  28. package/dist/jenkins-gitops-rollouts/schemas.d.ts +217 -0
  29. package/dist/jenkins-gitops-rollouts/schemas.js +131 -0
  30. package/dist/jenkins-gitops-rollouts/schemas.js.map +1 -0
  31. package/dist/jenkins-jobs/commands.d.ts +2 -0
  32. package/dist/jenkins-jobs/commands.js +33 -1
  33. package/dist/jenkins-jobs/commands.js.map +1 -1
  34. package/dist/publications/commands.js +24 -1
  35. package/dist/publications/commands.js.map +1 -1
  36. package/dist/repositories/commands.js +23 -1
  37. package/dist/repositories/commands.js.map +1 -1
  38. package/package.json +4 -4
  39. package/skills/numa-create-application/SKILL.md +163 -0
  40. package/skills/numa-create-application/agents/openai.yaml +4 -0
  41. package/skills/numa-create-application/evals/evals.json +75 -0
  42. package/skills/numa-create-application/references/checklist.md +130 -0
  43. package/skills/numa-create-application/references/inference-rules.md +69 -0
  44. package/skills/numa-create-application/references/numa-cli.md +232 -0
  45. package/skills/numa-create-application/references/promote-workflows.md +134 -0
  46. package/skills/numa-create-application/scripts/inspect-project.mjs +243 -0
@@ -0,0 +1,232 @@
1
+ # Numa CLI 执行契约
2
+
3
+ ## 目录
4
+
5
+ - [命令解析](#命令解析)
6
+ - [只读发现](#只读发现)
7
+ - [安全输出](#安全输出)
8
+ - [入驻执行](#入驻执行)
9
+ - [恢复规则](#恢复规则)
10
+
11
+ ## 命令解析
12
+
13
+ 优先使用 PATH 中的 `numa`。缺失时建议用户安装 launcher,或用以下临时入口执行公开诊断:
14
+
15
+ ```bash
16
+ npx -y --prefer-online --registry=https://registry.npmjs.org/ @numa-tech/numa@latest --version
17
+ ```
18
+
19
+ 不要硬编码本地 Numa 源码路径。先执行 `numa commands --json`,以运行时命令目录为准;CLI 增加一等 cluster/database 命令后优先使用新命令。
20
+
21
+ ## 只读发现
22
+
23
+ 按需执行:
24
+
25
+ ```bash
26
+ numa --version
27
+ numa auth status --json
28
+ numa config --show --json
29
+ numa profile --json
30
+ numa commands --json
31
+ numa app capabilities --json
32
+ numa app options team --json
33
+ numa app options system --json
34
+ numa app options business-domain --json
35
+ numa app options environment --json
36
+ numa repository connection list --json
37
+ numa repository capabilities --connection <code> --consumer application-onboarding --json
38
+ numa repository namespace list --connection <code> --json
39
+ numa repository remote list --connection <code> --search <name> --json
40
+ numa app list --json
41
+ numa pipeline list --app <application-code> --json
42
+ ```
43
+
44
+ 兼容期仍可只读使用 `numa app scm`,但仓库生命周期必须使用顶层 Repository Control Plane:
45
+
46
+ ```bash
47
+ numa repository remote list --connection <connection-code> --search <repository-name> --json
48
+ ```
49
+
50
+ 团队选项可见不等于当前用户具有 `MEMBER+`。通过运行时 command catalog 找团队成员查询能力;缺失时用批准的只读平台 API,并把无法证明的成员资格标记为 `UNRESOLVED`。
51
+
52
+ ## 安全输出
53
+
54
+ `numa request --json` 可能包含响应 headers。永远在命令输出到工具日志前做字段 allowlist;不要输出 `.response.headers`:
55
+
56
+ ```bash
57
+ set -o pipefail
58
+ numa request /approved/read-only/path --json |
59
+ jq 'if .ok then {ok, status: .response.status, body: .response.body} else {ok, error} end'
60
+ ```
61
+
62
+ 对 body 继续裁剪到本次决策需要的字段。禁止显示 token cache、`set-cookie`、Authorization、credential、password、secret、private key、完整 connection string。
63
+
64
+ `app publication attach --json` 的安全契约只包含 session/plan 标识、state、tree/manifest 计数、
65
+ digest、幂等键和恢复证据;不得输出完整 session draft/plan、manifest path、`sourceRoot`、
66
+ `contentBase64` 或 provider payload。
67
+
68
+ attach 的 POST 返回 500/504、连接中断或 2xx body 无法解析时,CLI 必须用原值有界轮询精确只读
69
+ resolve endpoint;手工恢复命令为:
70
+
71
+ ```bash
72
+ numa app publication resolve <session-no> --client-request-id <original-id> --json
73
+ ```
74
+
75
+ 该命令不上传 bundle、不重放 attach。200 必须是同一 session 的 `APPLICATION_ONBOARDING` binding
76
+ 且 `replayed=true`;retryable 404 `ONBOARDING_PUBLICATION_PLAN_NOT_RESOLVED` 表示可稍后重查,不能
77
+ 改用新 key 或新 session。
78
+
79
+ ## Repository Control Plane
80
+
81
+ 缺少远端仓库且计划由 Application Onboarding 消费时,先幂等创建空 session,再发现 namespace/capability 和创建不可变计划:
82
+
83
+ ```bash
84
+ numa app begin --mode new \
85
+ --idempotency-key begin-order-service --yes --json
86
+ numa repository capabilities \
87
+ --connection codeup-main --consumer application-onboarding --json
88
+ numa repository namespace list \
89
+ --connection codeup-main --search 'numa/framework' --json
90
+ numa repository remote list \
91
+ --connection codeup-main --search order-service --json
92
+ numa repository plan create \
93
+ --connection codeup-main --namespace 123 --name order-service \
94
+ --visibility private --initialization readme \
95
+ --consumer application-onboarding --consumer-ref <begin返回的session-no> \
96
+ --owner-team-id 12 --json
97
+ numa repository plan get <plan-no> --json
98
+ numa repository create \
99
+ --plan-no <plan-no> --plan-hash <64-hex> \
100
+ --client-request-id <stable-key> --idempotency-key <same-key> \
101
+ --yes --json
102
+ ```
103
+
104
+ Repository `create` 只负责远端生命周期和 catalog 一致性,不会隐式读取或推送本地目录。Codeup 的本地历史发布使用 `README` 初始化,随后由 Publication CAS 精确替换 tree;不要创建空仓后直接 Git push。
105
+
106
+ 独立仓库发布可使用顶层 `publication scan/plan/apply/status/events`。Application Onboarding 不得把这种 `STANDALONE/JWT` plan 冒充 child;必须在 parent 等待 `ATTACH_PUBLICATION_PLAN` 后调用:
107
+
108
+ ```bash
109
+ numa app publication attach <session-no> \
110
+ --source <clean-git-root> \
111
+ --commit-message 'feat: publish existing project' \
112
+ --client-request-id <stable-child-key> \
113
+ --idempotency-key <same-key> --yes --json
114
+ ```
115
+
116
+ CLI 只从 clean Git HEAD 构造受限 bundle,输出不回显文件内容或本地绝对路径。命令不存在时按 promote workflow 标记 `MANUAL_BRIDGE`,不能把普通 Git 推送写成平台功能。
117
+
118
+ ## 入驻执行
119
+
120
+ 普通接入可使用 schemaVersion 2;创建并 promote 已有本地项目必须使用 schemaVersion 3,把 Publication、Jenkins Job、GitOps、Observer 和首次 build 拆成独立 gate:
121
+
122
+ ```json
123
+ {
124
+ "schemaVersion": 3,
125
+ "mode": "NEW",
126
+ "catalogStrategy": "CREATE_NEW",
127
+ "application": {
128
+ "code": "order-service",
129
+ "name": "Order Service",
130
+ "teamId": 12,
131
+ "systemId": 7,
132
+ "defaultEnvironmentId": 2
133
+ },
134
+ "repositoryStrategy": "CREATE_NEW",
135
+ "repositoryPlanRef": {
136
+ "planNo": "rplan_...",
137
+ "planHash": "64-lowercase-hex"
138
+ },
139
+ "projectStrategy": "PUBLISH_PROJECT",
140
+ "technology": {
141
+ "stackCode": "java-mci",
142
+ "templateCode": "java-mci-standalone",
143
+ "repoMode": "STANDALONE",
144
+ "parameters": {}
145
+ },
146
+ "delivery": {
147
+ "jenkinsRequired": true,
148
+ "gitopsRequired": true,
149
+ "nacosRequired": false,
150
+ "tiers": ["DEVELOP"],
151
+ "jenkins": {
152
+ "operation": "CREATE_MULTIBRANCH",
153
+ "instanceCode": "numa",
154
+ "parentFullName": "mci/develop",
155
+ "jobName": "order-service",
156
+ "tier": "DEVELOP",
157
+ "defaultBranch": "main",
158
+ "jenkinsfilePath": "Jenkinsfile",
159
+ "scanMode": "NONE"
160
+ },
161
+ "targetKey": "develop",
162
+ "profileCode": "MCI_FLUX_V1",
163
+ "profileVersion": "1",
164
+ "structuralSpec": { "workloadKind": "SPRING", "service": { "port": 8080 } },
165
+ "observerScopeIntent": {
166
+ "kustomizationNamespace": "flux-system",
167
+ "kustomizationName": "develop",
168
+ "helmReleaseNamespace": "develop",
169
+ "helmReleaseName": "order-service",
170
+ "workloadNamespace": "develop",
171
+ "workloadKind": "Deployment",
172
+ "workloadName": "order-service"
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ `CREATE_NEW` 使用 `repositoryPlanRef {planNo,planHash}`;`ATTACH_EXISTING` 使用 `repositoryRef {repositoryKey,expectedHead}`。已有本地源码但无 remote 时,先锁 Repository plan,再创建 v3 parent;不要用受管模板覆盖本地项目,也不要另建第二个 `ATTACH_EXISTING` session。
179
+
180
+ `PUBLISH_PROJECT` 必须接续 `app begin` 创建的同一会话并始终先 plan:
181
+
182
+ ```bash
183
+ numa app onboard --session <session-no> --spec <spec-file> --plan --json
184
+ numa app apply <session-no> --idempotency-key <stable-key> --json
185
+ numa app inspect <session-no> --watch --after-sequence <last-sequence> --json
186
+ numa app publication attach <session-no> --source <git-root> --commit-message <message> --client-request-id <key> --idempotency-key <key> --yes --json
187
+ numa app jenkins-binding confirm <session-no> --binding-id <id> --expected-version <version> --idempotency-key <key> --yes --json
188
+ numa app observer-scope configure <session-no> --service-binding-id <id> --kustomization-namespace <ns> --kustomization-name <name> --helm-release-namespace <ns> --helm-release-name <name> --workload-namespace <ns> --workload-kind <kind> --workload-name <name> --expected-version <version> --idempotency-key <key> --yes --json
189
+ numa app first-build decide <session-no> --decision defer --idempotency-key <key> --yes --json
190
+ numa pipeline inspect <application-code> --json
191
+ ```
192
+
193
+ Jenkins Job Control Plane 使用 `numa jenkins job plan .../apply/status/events`;默认 `scanMode=NONE`,`INDEX_ONLY` 也必须由服务端 NoTrigger 验证。`app jenkins-binding confirm` 只确认关系,不触发 build。首次 build 必须单独执行 `app first-build decide --decision trigger`;生产场景切换到 `numa-jenkins-deployment` 并提供真实审批证据。
194
+
195
+ legacy Jenkins image writer 迁移与 rollout 是两个独立 gate。只有现有 overlay 缺 writer pointers 时才在 GitOps materialize/apply 前做 migration;普通 GitOps plan 已产生合规 pointers 时跳过。当前 `code-index` 生产 target 是 `prd`、Jenkins instance 是 `numa`、approved shared-library commit 是 `230e0f00444dfd1874e1ab3e624381341a0c31c5`;当前没有 service binding,migration plan 不带 `--service-id`:
196
+
197
+ ```bash
198
+ numa gitops service legacy-image-migration plan --application code-index --target prd --profile MCI_FLUX_V1 --json
199
+ numa gitops service apply <migration-plan-number> --plan-hash <64hex> --idempotency-key code-index-prd-image-migration-1 --yes --json
200
+ numa jenkins gitops-rollout plan enable-platform --application code-index --instance numa --shared-library-revision 230e0f00444dfd1874e1ab3e624381341a0c31c5 --idempotency-key code-index-numa-rollout-plan-1 --yes --json
201
+ numa jenkins gitops-rollout plan get <rollout-plan-no> --json
202
+ numa jenkins gitops-rollout apply --plan-no <rollout-plan-no> --plan-hash <64hex> --idempotency-key code-index-numa-rollout-apply-1 --yes --json
203
+ numa jenkins gitops-rollout list --application code-index --instance numa --json
204
+ numa jenkins gitops-rollout get <rollout-no> --json
205
+ numa jenkins gitops-rollout readiness <rollout-no> --desired-mode platform --expected-version <version> --operator-action-reference <change-record> --idempotency-key code-index-numa-readiness-1 --yes --json
206
+ ```
207
+
208
+ migration plan 只接受 stable references,不接受 YAML/path/image/secret;READY 后复用 `gitops service apply`。rollout 所有写入要求 Idempotency-Key 与 `--yes`。`readiness` 由 CLI 从 GET 回传 exact tuple,固定为 `MANUAL_OPERATOR_ATTESTED`,不接受 secret/XML/env map/credential value,也不表示 Jenkins live-read。
209
+
210
+ 不要把数据库密码、SCM token 或其他 secret 放进 spec、delivery.parameters 或 build parameters。
211
+
212
+ ## 恢复规则
213
+
214
+ - 保存 `sessionNo`、`revision`、`planHash`、idempotency key、last sequence。
215
+ - 网络失败或结果未知时先使用正常 profile HTTPS 网关执行下面的只读恢复;同一次提交只复用原 idempotency key:
216
+
217
+ ```bash
218
+ numa repository status --client-request-id <original-id> --json
219
+ numa publication status --client-request-id <original-id> --json
220
+ numa jenkins job status --client-request-id <original-id> --json
221
+ numa app publication resolve <session-no> --client-request-id <original-id> --json
222
+ numa app inspect <session-no> --after-sequence <last-sequence> --json
223
+ ```
224
+
225
+ CLI 只有在写前 event cursor 之后观察到与本次 planHash/精确 intent 匹配的新持久事件时,才可能返回 `recovered_from_status=true`;既有 `WAITING_EXTERNAL` 或旧输出字段不能作为恢复证据。Repository、Publication、Jenkins 的 resolved run 还必须匹配请求 planNo 和响应中可用的稳定 workload identity。
226
+ - HTTP 2xx 后客户端 schema 解析失败不代表服务端失败。先 list/get 核实资源,禁止直接重放 POST。
227
+ - legacy migration plan 无 Idempotency-Key/resolve-by-key,结果未知时 fail closed。Rollout plan create 也不按 key resolve;apply 必须用写前 list baseline 与写后 exact application/instance/plan mode/new version 恢复;readiness 必须用同 rollout 的 exact reference/mode/status/identity/new version 恢复。预存 READY/LEGACY 不是成功证据。
228
+ - `WAITING_EXTERNAL` 使用 `resume`;可重入失败使用 `retry`;HEAD 移动使用 `reassess`。按服务端 recovery action 选择,不自行跳步骤。
229
+ - 生产 `pipeline build`、stop、retry 需要独立审批 ID 或具体 production reason;本 skill 默认只创建/核验流水线。
230
+ - 不启动或遗留 `kubectl port-forward`、SSH tunnel 或后台代理来绕过网关;只读恢复仍失败时报告 profile/API origin、request ID 和上述恢复命令,等待平台恢复。
231
+
232
+ 命令块必须是可复制的真实 shell:续行只使用行尾反斜杠,禁止字面 diff `+`/`-` 前缀。
@@ -0,0 +1,134 @@
1
+ # 应用创建与 Promote 工作流
2
+
3
+ ## 目录
4
+
5
+ - [能力自述](#能力自述)
6
+ - [源码状态路由](#源码状态路由)
7
+ - [完整父状态机](#完整父状态机)
8
+ - [安全与恢复](#安全与恢复)
9
+ - [验收](#验收)
10
+
11
+ ## 能力自述
12
+
13
+ 每次开始时基于运行时 command catalog 和 capability API 输出以下矩阵,不能依赖 skill 编写时的版本记忆:
14
+
15
+ | 能力 | 证明方式 | 不可用时 |
16
+ |---|---|---|
17
+ | Repository plan/run | `repository capabilities` 与命令目录 | 停止仓库写入 |
18
+ | Project Publication | 服务端规范 action `PLAN_PUBLICATION`,并由 command catalog 证明 `publication`、`app publication attach/resolve` 本地命令存在;`resolve` 不是独立服务端 action | 标记 `MANUAL_BRIDGE`,请求独立授权 |
19
+ | Jenkins Job plan/run | job plan/provision/status 命令 | `WAITING_EXTERNAL`,不得直接调用 Jenkins |
20
+ | Application onboarding | v2/v3 capabilities 与 plan | 不创建 Application |
21
+ | GitOps plan/apply | target/profile/service 命令 | 保留已完成 child,停止部署 |
22
+ | Legacy image writer migration | `gitops service legacy-image-migration plan` 命令目录;不需要独立 server capability action | 保留 binding,不直写 YAML/path/image |
23
+ | Jenkins GitOps rollout | `jenkins gitops-rollout plan/apply/list/get/readiness` 命令目录;readiness 必须自述 `MANUAL_OPERATOR_ATTESTED` | 保持 fail-closed gate,不触发首次 build |
24
+ | Observer scope/evidence | binding/scope/evidence read API | 不把 target 或 Application 声称为 ACTIVE |
25
+ | Pipeline deploy | confirmed binding 与生产审批 | 不触发 build |
26
+
27
+ 能力自述必须说明:当前 skill 可以安全完成哪些步骤、哪些步骤需要管理员、哪些步骤会触发外部写入,以及这次默认不会执行什么。
28
+ Application onboarding 以 `numa app capabilities --json` 的服务端动作自述为准,并与
29
+ `numa commands --json` 交叉验证;命令存在而服务端返回 501 时标记 `UNAVAILABLE`。
30
+
31
+ ## 源码状态路由
32
+
33
+ ### REMOTE_TRACKED
34
+
35
+ 1. 解析 remote 到平台 connection/repository。
36
+ 2. 读取 provider externalId、default branch 和 remote HEAD。
37
+ 3. 要求本地 clean HEAD 与远端目标 HEAD 一致。
38
+ 4. 生成 `USE_EXISTING`/`REGISTER_EXISTING` plan 或直接使用受管 repositoryRef。
39
+
40
+ ### LOCAL_HISTORY_WITHOUT_REMOTE
41
+
42
+ 适用于已有提交历史、clean HEAD、无任何 remote 的项目:
43
+
44
+ 1. 从 Jenkins `APP_NAME`、包名和目录名推断 application code;冲突时停止确认。
45
+ 2. 查询 SCM connection、namespace、同名 remote、CREATE capability 和初始化限制。
46
+ 3. 生成 Repository `CREATE` plan。仓库名默认等于稳定 application code,visibility 默认 PRIVATE。初始化方式必须来自 provider capability:Codeup 无空仓安全首次提交能力时使用受管 `README` 初始化,再由 publication CAS 更新同一 `README.md` 并创建其余清单文件;不得先建空仓再绕过控制平面推送。
47
+ 4. 本地只读生成受限 publication manifest;manifest 锁定 source HEAD、mode、文件数量、总字节、每文件 digest 和整体 digest,不包含内容或凭据。若包含 `100755` 且 provider=Codeup,安全连接摘要必须证明 `codeupCloneCredentialConfigured=true`;否则以 `PUBLICATION_EXECUTABLE_MODE_UNSUPPORTED` 停止,不能把 Personal AT 当 HTTPS Git 密码。
48
+ 5. 用 `app begin` 取得 session 后,在同一 session 创建 schemaVersion=3 onboarding parent plan;固定 `mode=NEW`、catalog/repository=`CREATE_NEW`、project=`PUBLISH_PROJECT`,并引用绑定该 session 的 Repository plan/ref。Repository child 完成后 parent 停在 `WAITING_EXTERNAL / ATTACH_PUBLICATION_PLAN`,此时才用 `app publication attach` 上传受控 bundle;服务端把 child consumer 固定为 `APPLICATION_ONBOARDING/<sessionNo>`,验证 parent planHash 和同一 manifest digest。客户端不能自报 consumer、remote URL、branch 或 provider credential。
49
+ 6. 远端创建或提交结果未知时复用同一 idempotency key,通过 run/events reconcile;不能直接重放成第二个仓库或第二次提交。
50
+ 7. 读回 remote HEAD/tree evidence 后继续同一个 onboarding session;不得另建 `ATTACH_EXISTING` 会话,也不得把独立 `STANDALONE` publication plan 附着到 parent。
51
+
52
+ 若 publication API 尚未发布,仍生成完整计划并标记 `MANUAL_BRIDGE`。只有用户单独确认后才能使用本机 Git 身份推送;推送前后必须验证 remote URL、branch、HEAD,且不得把 credential 写入命令、remote URL 或日志。人工桥接不是 skill 的默认路径,也不能被报告成平台闭环。
53
+
54
+ ### DIRTY_WORKTREE / UNVERSIONED_DIRECTORY
55
+
56
+ - dirty 时只列出变更数量,不读取敏感文件;停止 apply,让用户决定提交范围。
57
+ - 非 Git 目录只能选择受管模板 NEW,或先由用户建立明确版本历史;不要自动 `git init`/commit。
58
+
59
+ ## 完整父状态机
60
+
61
+ ```text
62
+ DISCOVER_AND_CLASSIFY
63
+ BEGIN_APPLICATION_SESSION
64
+ PLAN_REPOSITORY
65
+ PLAN_APPLICATION_V3
66
+ RUN_APPLICATION_PARENT
67
+ RUN_REPOSITORY_CHILD
68
+ WAIT_ATTACH_PUBLICATION_PLAN
69
+ ATTACH_PROJECT_PUBLICATION_PLAN
70
+ RUN_PROJECT_PUBLICATION_CHILD
71
+ VERIFY_REMOTE_HEAD
72
+ PLAN_JENKINS_JOB
73
+ RUN_JENKINS_JOB_NO_BUILD
74
+ SYNC_INVENTORY_AND_CONFIRM_BINDING
75
+ PLAN_LEGACY_IMAGE_WRITER_MIGRATION_IF_REQUIRED
76
+ WAIT_AUTHENTICATED_MIGRATION_APPLY
77
+ PLAN_GITOPS_STRUCTURE
78
+ WAIT_AUTHENTICATED_GITOPS_APPLY
79
+ PLAN_AND_APPLY_JENKINS_GITOPS_ROLLOUT
80
+ WAIT_TRUSTED_MANUAL_READINESS
81
+ CONFIGURE_OBSERVER_SCOPE
82
+ WAIT_FLUX_OBSERVER_ATTESTATION
83
+ WAIT_EXPLICIT_FIRST_BUILD
84
+ COMPLETED
85
+ ```
86
+
87
+ 已部署应用可采用 `ATTACH_APPLICATION`,但必须验证团队、repository、Jenkins 与运行态身份一致。Candidate adopt 不得创建一个与 onboarding reservation 冲突的第二个 Application。
88
+
89
+ Jenkins Job 创建、配置和 `INDEX_ONLY` scan 必须验证不会触发 build;默认 `scanMode=NONE`。首次 build 是独立 promote 决策:`DEFER` 永不触发且 parent 保持 `WAITING_EXTERNAL`,`TRIGGER` 必须使用 confirmed binding。生产 build 转入 `numa-jenkins-deployment`,要求真实审批依据。
90
+
91
+ ### Legacy writer 与 Jenkins rollout gate
92
+
93
+ 当只读证据证明现有 overlay 缺少 Jenkins writer pointers 时,migration 是受治理 GitOps materialize/apply 的前置。如果普通 GitOps plan 已生成合规 pointers,则不执行 migration。当前 `code-index`
94
+ 的生产事实为 target=`prd`、Jenkins instance=`numa`、approved shared-library commit=
95
+ `230e0f00444dfd1874e1ab3e624381341a0c31c5`;当前没有 service binding,因此 plan 不得猜测
96
+ `--service-id`:
97
+
98
+ ```bash
99
+ numa gitops service legacy-image-migration plan --application code-index --target prd --profile MCI_FLUX_V1 --json
100
+ numa gitops service apply <migration-plan-number> --plan-hash <64hex> --idempotency-key code-index-prd-image-migration-1 --yes --json
101
+ numa jenkins gitops-rollout plan enable-platform --application code-index --instance numa --shared-library-revision 230e0f00444dfd1874e1ab3e624381341a0c31c5 --idempotency-key code-index-numa-rollout-plan-1 --yes --json
102
+ numa jenkins gitops-rollout plan get <rollout-plan-no> --json
103
+ numa jenkins gitops-rollout apply --plan-no <rollout-plan-no> --plan-hash <64hex> --idempotency-key code-index-numa-rollout-apply-1 --yes --json
104
+ numa jenkins gitops-rollout get <rollout-no> --json
105
+ numa jenkins gitops-rollout readiness <rollout-no> --desired-mode platform --expected-version <version> --operator-action-reference <change-record> --idempotency-key code-index-numa-readiness-1 --yes --json
106
+ ```
107
+
108
+ migration plan 请求不接受 YAML/path/image/secret/credential,CLI 也不输出文件 path/diff/pointer。
109
+ rollout apply 只持久化 gate,不连接 Jenkins、不改全局配置、不触发 build。readiness 是
110
+ trusted manual attestation,不是 live-read;CLI 从 GET 响应回传 exact tuple 与 credential reference names,
111
+ 不接收 credential value、raw XML 或 env map。
112
+
113
+ ## 安全与恢复
114
+
115
+ - CLI 从不接受 SCM/Jenkins credential 参数;凭据由平台引用。
116
+ - publication 拒绝 `.git`、凭据文件、Secret、symlink、socket/device、绝对/父路径和超限 bundle。
117
+ - Codeup Personal AT 只证明 OpenAPI 能力;包含 `100755` 的 tree 还要求平台已托管独立 HTTPS clone username/password。只读检查安全 readiness boolean,缺失时以 `PUBLICATION_EXECUTABLE_MODE_UNSUPPORTED` 停止,不把 AT 当 Git password。
118
+ - 每个 child 保存 plan/hash、run number、idempotency key、last event sequence;onboarding publication child 只能通过 session-scoped bridge 创建。
119
+ - HTTP 2xx 后 schema 解析失败属于结果未知;先 list/status/get,不能直接重放。
120
+ - legacy migration plan 没有 Idempotency-Key/resolve-by-key,未知结果必须 fail closed。Rollout plan 也不按 key resolve;apply 只在写前 baseline 之后出现 exact application/instance/plan mode 且新 version 时恢复;readiness 只在同 rollout 出现 exact reference/mode/status/identity 且新 version 时恢复。预存 READY/LEGACY 不构成证据。
121
+ - 普通 profile HTTPS 网关失败时,Repository/standalone Publication/Jenkins 先按原 clientRequestId 查 `status`,普通 session action 先 `app inspect`;publication attach 必须先按原 `sessionNo/clientRequestId` 执行 `app publication resolve`。其 retryable 404 只能稍后重查,禁止自动重放 attach;不要启动或遗留 port-forward/tunnel 旁路。
122
+ - session action 的自动恢复必须以写前 event cursor 为基线,只接受 cursor 之后与本次 planHash/精确 intent 匹配的新持久事件;既有 `WAITING_EXTERNAL` 或旧 observer/build/binding 字段不是本次写成功的证据。child run resolve 必须至少匹配请求 planNo,并校验响应中可用的 repository/workload identity。
123
+ - Jenkins scan 不等于 build;观察到意外 build 时停止父流程并报告。
124
+ - GitOps target 只有 topology validation 时保持 DRAFT;Jenkins attestation 最多推进 VERIFYING,只有独立 Flux Observer evidence 能推进 ACTIVE。
125
+
126
+ ## 验收
127
+
128
+ - 远端仓库唯一,默认分支、visibility、owner team 与确认一致。
129
+ - remote commit/tree 与 publication manifest evidence 一致。
130
+ - Application 只创建/接管一个,Repository 关系正确。
131
+ - Multibranch Job 配置 digest 与计划一致,未意外触发 build。
132
+ - Jenkins binding 为 CONFIRMED,目标 branch 可构建。
133
+ - GitOps service binding 和 exact Observer scope 存在;configRevision 与 observedRevision 一致后才 ACTIVE。
134
+ - 未经独立生产授权不得触发 build;触发后必须以 Jenkins 终态和 Flux/Observer evidence 验收。
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
5
+ import { basename, join, resolve } from 'node:path';
6
+
7
+ const root = resolve(process.argv[2] ?? process.cwd());
8
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
9
+ process.stderr.write('Project directory does not exist or is not a directory.\n');
10
+ process.exit(2);
11
+ }
12
+
13
+ const excludedDirectories = new Set([
14
+ '.git', '.idea', '.next', '.nuxt', '.output', '.turbo', '.venv',
15
+ '.superpowers', '.worktrees', 'coverage', 'dist', 'node_modules', 'out',
16
+ 'target', 'test-results'
17
+ ]);
18
+ const sensitiveNames = /^(?:\.env(?:\..*)?|.*(?:secret|credential|private[-_.]?key|token).*)$/iu;
19
+ const maximumFiles = 5_000;
20
+ const maximumDepth = 7;
21
+ const files = [];
22
+
23
+ function walk(directory, depth = 0) {
24
+ if (depth > maximumDepth || files.length >= maximumFiles) return;
25
+ let entries = [];
26
+ try {
27
+ entries = readdirSync(directory, { withFileTypes: true });
28
+ } catch {
29
+ return;
30
+ }
31
+ for (const entry of entries) {
32
+ if (files.length >= maximumFiles) break;
33
+ if (entry.isDirectory()) {
34
+ if (!excludedDirectories.has(entry.name) && !sensitiveNames.test(entry.name)) {
35
+ walk(join(directory, entry.name), depth + 1);
36
+ }
37
+ continue;
38
+ }
39
+ if (entry.isFile() && !sensitiveNames.test(entry.name)) files.push(join(directory, entry.name));
40
+ }
41
+ }
42
+
43
+ walk(root);
44
+ const relativeFiles = files.map((file) => file.slice(root.length + 1));
45
+ const fileSet = new Set(relativeFiles);
46
+
47
+ function text(relativePath, maximumBytes = 1_000_000) {
48
+ if (!fileSet.has(relativePath)) return null;
49
+ try {
50
+ const value = readFileSync(join(root, relativePath));
51
+ return value.byteLength <= maximumBytes ? value.toString('utf8') : null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function json(relativePath) {
58
+ const value = text(relativePath);
59
+ if (!value) return null;
60
+ try {
61
+ return JSON.parse(value);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function git(args) {
68
+ try {
69
+ return execFileSync('git', args, {
70
+ cwd: root,
71
+ encoding: 'utf8',
72
+ stdio: ['ignore', 'pipe', 'ignore'],
73
+ maxBuffer: 2_000_000
74
+ }).trim();
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function sanitizeRemote(value) {
81
+ if (!value) return null;
82
+ try {
83
+ const parsed = new URL(value);
84
+ parsed.username = '';
85
+ parsed.password = '';
86
+ parsed.search = '';
87
+ parsed.hash = '';
88
+ return parsed.toString();
89
+ } catch {
90
+ return value
91
+ .replace(/^[^@\s]+@([^:]+):/u, '$1:')
92
+ .replace(/([?&](?:access_?token|token|password|secret)=)[^&#\s]+/giu, '$1[REDACTED]');
93
+ }
94
+ }
95
+
96
+ function slug(value) {
97
+ return String(value ?? '')
98
+ .trim()
99
+ .replace(/^@[^/]+\//u, '')
100
+ .replace(/\.git$/u, '')
101
+ .split(/[/:]/u)
102
+ .at(-1)
103
+ ?.toLowerCase()
104
+ .replace(/[^a-z0-9]+/gu, '-')
105
+ .replace(/^-+|-+$/gu, '') || null;
106
+ }
107
+
108
+ const packageFiles = relativeFiles.filter((file) => /(^|\/)package\.json$/u.test(file));
109
+ const manifests = packageFiles.flatMap((file) => {
110
+ const value = json(file);
111
+ return value && typeof value === 'object' ? [{ file, value }] : [];
112
+ });
113
+ const rootPackage = manifests.find((entry) => entry.file === 'package.json')?.value ?? null;
114
+ const dependencies = new Set();
115
+ for (const { value } of manifests) {
116
+ for (const section of ['dependencies', 'devDependencies', 'peerDependencies']) {
117
+ for (const name of Object.keys(value[section] ?? {})) dependencies.add(name);
118
+ }
119
+ }
120
+
121
+ const jenkins = text('Jenkinsfile') ?? '';
122
+ const dockerfile = text('Dockerfile') ?? '';
123
+ const jenkinsVariables = {};
124
+ for (const key of ['APP_NAME', 'APP_HOME', 'BUILD_FLOW', 'TRIGGER_BUILD', 'NODE_VERSION', 'JDK_VERSION', 'SONAR_SCAN']) {
125
+ const match = jenkins.match(new RegExp(`\\b${key}\\s*=\\s*["']([^"']+)["']`, 'u'));
126
+ if (match) jenkinsVariables[key] = match[1];
127
+ }
128
+
129
+ const remoteNames = (git(['remote']) ?? '').split(/\r?\n/u).filter(Boolean);
130
+ const remotes = remoteNames.map((name) => ({
131
+ name,
132
+ url: sanitizeRemote(git(['remote', 'get-url', name]))
133
+ }));
134
+ const primaryRemote = remotes.find((item) => item.name === 'origin') ?? remotes[0] ?? null;
135
+ const statusLines = (git(['status', '--porcelain', '--untracked-files=all']) ?? '')
136
+ .split(/\r?\n/u).filter(Boolean);
137
+ const branch = git(['branch', '--show-current']);
138
+ const head = git(['rev-parse', 'HEAD']);
139
+ const upstream = git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
140
+ const isGitRepository = git(['rev-parse', '--is-inside-work-tree']) === 'true';
141
+ const trackedPathCount = (git(['ls-files', '-z']) ?? '').split('\0').filter(Boolean).length;
142
+ const executableTrackedPathCount = (git(['ls-files', '-s', '-z']) ?? '')
143
+ .split('\0')
144
+ .filter((entry) => entry.startsWith('100755 '))
145
+ .length;
146
+ const sourceState = !isGitRepository ? 'UNVERSIONED_DIRECTORY'
147
+ : statusLines.length > 0 ? 'DIRTY_WORKTREE'
148
+ : remoteNames.length === 0 && head ? 'LOCAL_HISTORY_WITHOUT_REMOTE'
149
+ : remoteNames.length > 0 && !upstream ? 'REMOTE_WITHOUT_UPSTREAM'
150
+ : 'REMOTE_TRACKED';
151
+
152
+ const technologies = [];
153
+ function technology(code, evidence) {
154
+ technologies.push({ code, evidence });
155
+ }
156
+ if (fileSet.has('pom.xml')) technology('java-maven', 'pom.xml');
157
+ if (fileSet.has('settings.gradle.kts') || fileSet.has('build.gradle.kts')) technology('java-gradle', 'Gradle Kotlin DSL');
158
+ if (fileSet.has('go.mod')) technology('go', 'go.mod');
159
+ if (fileSet.has('pyproject.toml')) technology('python', 'pyproject.toml');
160
+ if (dependencies.has('@nestjs/core')) technology('nestjs', '@nestjs/core dependency');
161
+ if (dependencies.has('next')) technology('nextjs', 'next dependency');
162
+ if (dependencies.has('nuxt')) technology('nuxt', 'nuxt dependency');
163
+ if (dependencies.has('fastify')) technology('fastify', 'fastify dependency');
164
+ if (dependencies.has('express')) technology('express', 'express dependency');
165
+ if (dependencies.has('vite')) technology('vite', 'vite dependency');
166
+ if (rootPackage && technologies.length === 0) technology('node', 'package.json');
167
+
168
+ const databaseSignals = [];
169
+ const databasePackages = new Map([
170
+ ['pg', 'POSTGRESQL'], ['postgres', 'POSTGRESQL'], ['drizzle-orm', 'SQL'],
171
+ ['@prisma/client', 'SQL'], ['mysql2', 'MYSQL'], ['mariadb', 'MYSQL'],
172
+ ['mongodb', 'MONGODB'], ['mongoose', 'MONGODB'], ['better-sqlite3', 'SQLITE'], ['sqlite3', 'SQLITE']
173
+ ]);
174
+ for (const [name, engine] of databasePackages) {
175
+ if (dependencies.has(name)) databaseSignals.push({ engine, evidence: `dependency:${name}` });
176
+ }
177
+ for (const file of relativeFiles) {
178
+ if (/\/(?:drizzle|prisma|migrations?)\//u.test(`/${file}`)) {
179
+ databaseSignals.push({ engine: 'UNKNOWN_SQL', evidence: `path:${file}` });
180
+ if (databaseSignals.length >= 20) break;
181
+ }
182
+ }
183
+
184
+ const packageManager = fileSet.has('pnpm-lock.yaml') ? 'pnpm'
185
+ : fileSet.has('yarn.lock') ? 'yarn'
186
+ : fileSet.has('package-lock.json') ? 'npm'
187
+ : fileSet.has('bun.lockb') || fileSet.has('bun.lock') ? 'bun' : null;
188
+ const remoteSlug = slug(primaryRemote?.url);
189
+ const packageSlug = slug(rootPackage?.name);
190
+ const appCodeCandidate = slug(jenkinsVariables.APP_NAME) ?? remoteSlug ?? packageSlug ?? slug(basename(root));
191
+
192
+ const result = {
193
+ schemaVersion: 1,
194
+ scan: {
195
+ directoryName: basename(root),
196
+ filesConsidered: relativeFiles.length,
197
+ truncated: files.length >= maximumFiles,
198
+ maximumDepth,
199
+ sensitiveFilesRead: false
200
+ },
201
+ application: {
202
+ codeCandidate: appCodeCandidate,
203
+ evidence: jenkinsVariables.APP_NAME ? 'Jenkinsfile:APP_NAME'
204
+ : remoteSlug ? 'git:primary-remote' : rootPackage?.name ? 'package.json:name' : 'directory-name',
205
+ packageName: rootPackage?.name ?? null
206
+ },
207
+ git: {
208
+ repository: isGitRepository,
209
+ sourceState,
210
+ branch: branch || null,
211
+ head: head || null,
212
+ upstream: upstream || null,
213
+ dirty: statusLines.length > 0,
214
+ changedPathCount: statusLines.length,
215
+ trackedPathCount,
216
+ executableTrackedPathCount,
217
+ remotes
218
+ },
219
+ build: {
220
+ packageManager,
221
+ packageFiles,
222
+ technologies,
223
+ jenkinsfile: fileSet.has('Jenkinsfile'),
224
+ jenkinsVariables,
225
+ buildEntry: /\bbuildEntry\(\)/u.test(jenkins),
226
+ dockerfile: fileSet.has('Dockerfile'),
227
+ dockerExposedPorts: [...dockerfile.matchAll(/^EXPOSE\s+([^\r\n#]+)/gmu)].map((match) => match[1].trim()),
228
+ hasCompose: fileSet.has('compose.yaml') || fileSet.has('docker-compose.yml')
229
+ },
230
+ database: {
231
+ signals: databaseSignals,
232
+ recommendation: databaseSignals.length === 0 ? 'NONE'
233
+ : databaseSignals.some((item) => item.engine === 'POSTGRESQL') ? 'POSTGRESQL'
234
+ : databaseSignals.every((item) => item.engine === databaseSignals[0]?.engine) ? databaseSignals[0]?.engine : 'REVIEW'
235
+ },
236
+ deployment: {
237
+ hasHelm: relativeFiles.some((file) => /(^|\/)Chart\.ya?ml$/u.test(file)),
238
+ hasKubernetesManifests: relativeFiles.some((file) => /(^|\/)(?:k8s|kubernetes|helm)\//u.test(file)),
239
+ hasCloudflareWorker: relativeFiles.some((file) => /(^|\/)wrangler(?:\.[^.]+)?\.(?:toml|jsonc?)$/u.test(file))
240
+ }
241
+ };
242
+
243
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);