@whyshu/argent-engine 0.0.0-stage → 0.6.0

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 (305) hide show
  1. package/MANIFEST.json +307 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +575 -0
  55. package/engine/scripts/billing-mode-daily.sh +12 -0
  56. package/engine/scripts/build-engine-package.mjs +123 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +377 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/config-explain.mjs +278 -0
  81. package/engine/scripts/configure-provider.rb +29 -0
  82. package/engine/scripts/customer-config-migrate.mjs +179 -0
  83. package/engine/scripts/customer-preflight.py +892 -0
  84. package/engine/scripts/customer-readiness.sh +134 -0
  85. package/engine/scripts/daily-report-inbox.py +242 -0
  86. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  87. package/engine/scripts/daily-report.mjs +1310 -0
  88. package/engine/scripts/daily-report.sh +46 -0
  89. package/engine/scripts/data/lip-product-dict.json +306 -0
  90. package/engine/scripts/deliver-customer.mjs +932 -0
  91. package/engine/scripts/delivery-faces.txt +31 -0
  92. package/engine/scripts/desktop-trial.sh +146 -0
  93. package/engine/scripts/dev-window.sh +260 -0
  94. package/engine/scripts/drill-clean-machine.sh +315 -0
  95. package/engine/scripts/dsh-api-client.mjs +215 -0
  96. package/engine/scripts/dsh-backup-scope.sh +83 -0
  97. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  98. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  99. package/engine/scripts/dsh-plane.mjs +153 -0
  100. package/engine/scripts/dsh-portforward.py +223 -0
  101. package/engine/scripts/dsh-preflight.mjs +179 -0
  102. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  103. package/engine/scripts/dsh-release-watch.mjs +458 -0
  104. package/engine/scripts/dsh-remote-check.sh +99 -0
  105. package/engine/scripts/dsh-restart.sh +179 -0
  106. package/engine/scripts/dsh-restore.sh +111 -0
  107. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  108. package/engine/scripts/dsh-snapshot.mjs +116 -0
  109. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  110. package/engine/scripts/dsh-upgrade.mjs +612 -0
  111. package/engine/scripts/dsh-url.sh +86 -0
  112. package/engine/scripts/dsh-web-launch.mjs +155 -0
  113. package/engine/scripts/ensure-store-windows.py +509 -0
  114. package/engine/scripts/entitlements.py +458 -0
  115. package/engine/scripts/env_probe.py +411 -0
  116. package/engine/scripts/feishu-card.mjs +252 -0
  117. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  118. package/engine/scripts/flow-auto-retry.py +454 -0
  119. package/engine/scripts/flow-requirements.json +720 -0
  120. package/engine/scripts/flow-scaffold.py +275 -0
  121. package/engine/scripts/flow-timers-plist.sh +86 -0
  122. package/engine/scripts/flow-timers.sh +182 -0
  123. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  124. package/engine/scripts/flow-trigger.sh +571 -0
  125. package/engine/scripts/flow_billing.py +354 -0
  126. package/engine/scripts/flow_capability_stats.py +314 -0
  127. package/engine/scripts/flow_case.py +194 -0
  128. package/engine/scripts/flow_claim.py +497 -0
  129. package/engine/scripts/flow_config.py +1024 -0
  130. package/engine/scripts/flow_failure.py +588 -0
  131. package/engine/scripts/flow_learn.py +105 -0
  132. package/engine/scripts/flow_ledger.py +235 -0
  133. package/engine/scripts/flow_policy.py +238 -0
  134. package/engine/scripts/flow_preflight.py +339 -0
  135. package/engine/scripts/flow_registry.py +341 -0
  136. package/engine/scripts/flow_retry.py +304 -0
  137. package/engine/scripts/flow_return_to_ops.py +191 -0
  138. package/engine/scripts/flow_source.py +315 -0
  139. package/engine/scripts/flow_sources/__init__.py +53 -0
  140. package/engine/scripts/flow_sources/cases_json.py +106 -0
  141. package/engine/scripts/flow_sources/common.py +428 -0
  142. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  143. package/engine/scripts/flow_stale.py +185 -0
  144. package/engine/scripts/flow_status_reconcile.py +344 -0
  145. package/engine/scripts/flow_triage.py +849 -0
  146. package/engine/scripts/flow_write_verify.py +496 -0
  147. package/engine/scripts/fulfillment.js +586 -0
  148. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  149. package/engine/scripts/gen-preset-rows.mjs +259 -0
  150. package/engine/scripts/init-ml.sh +2275 -0
  151. package/engine/scripts/install-internal-role.sh +190 -0
  152. package/engine/scripts/install-license.mjs +641 -0
  153. package/engine/scripts/install-one-shot.mjs +665 -0
  154. package/engine/scripts/integration_contract.py +220 -0
  155. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  156. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  157. package/engine/scripts/invoice-mapping.example.json +40 -0
  158. package/engine/scripts/invoice-table.sh +194 -0
  159. package/engine/scripts/invoice_carrier_notice.py +279 -0
  160. package/engine/scripts/invoice_fail_closeout.py +297 -0
  161. package/engine/scripts/invoice_lip_runner.py +1508 -0
  162. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  163. package/engine/scripts/invoice_ready_audit.py +191 -0
  164. package/engine/scripts/invoice_runner.py +675 -0
  165. package/engine/scripts/invoice_stageb.py +516 -0
  166. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  167. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  168. package/engine/scripts/lease.py +377 -0
  169. package/engine/scripts/lib/agent-install.mjs +649 -0
  170. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  171. package/engine/scripts/lib/argent_datadir.py +262 -0
  172. package/engine/scripts/lib/argent_filelock.py +507 -0
  173. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  174. package/engine/scripts/lib/cdp_ports.py +197 -0
  175. package/engine/scripts/lib/customer-env.mjs +102 -0
  176. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  177. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  178. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  179. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  180. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  181. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  182. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  183. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  184. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  185. package/engine/scripts/lib/patch-compose.mjs +241 -0
  186. package/engine/scripts/lib/patch-target.mjs +35 -0
  187. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  188. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  189. package/engine/scripts/lib/preset-compat.mjs +127 -0
  190. package/engine/scripts/lib/preset-install.mjs +575 -0
  191. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  192. package/engine/scripts/lib/preset-sync.mjs +59 -0
  193. package/engine/scripts/lib/profile-retire.mjs +97 -0
  194. package/engine/scripts/lib/python-runtime.mjs +190 -0
  195. package/engine/scripts/lib/registry.mjs +72 -0
  196. package/engine/scripts/lib/repo-root.sh +56 -0
  197. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  198. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  199. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  200. package/engine/scripts/lib/settings-panels.mjs +816 -0
  201. package/engine/scripts/lib/stdio_utf8.py +86 -0
  202. package/engine/scripts/lib/wake-auth.mjs +392 -0
  203. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  204. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  205. package/engine/scripts/license-e2e-drill.mjs +426 -0
  206. package/engine/scripts/license-gate.mjs +245 -0
  207. package/engine/scripts/lip_backlog.py +171 -0
  208. package/engine/scripts/lip_table.py +185 -0
  209. package/engine/scripts/local-usage.mjs +214 -0
  210. package/engine/scripts/migrate-providers.mjs +127 -0
  211. package/engine/scripts/migrate-skills.sh +65 -0
  212. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  213. package/engine/scripts/ml-store-ids.json +26 -0
  214. package/engine/scripts/ml_artifact_attribution.py +333 -0
  215. package/engine/scripts/ml_capacity_probe.py +1158 -0
  216. package/engine/scripts/ml_hub_status.py +1556 -0
  217. package/engine/scripts/ml_schema_check.py +115 -0
  218. package/engine/scripts/ml_store_ids.py +139 -0
  219. package/engine/scripts/npm-publish-argent.mjs +499 -0
  220. package/engine/scripts/npm-publish-argent.sh +20 -0
  221. package/engine/scripts/orchestrator.py +13811 -0
  222. package/engine/scripts/pack-plugins.mjs +140 -0
  223. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  224. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  225. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  226. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  227. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  228. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  229. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  230. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  231. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  232. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  233. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  234. package/engine/scripts/plan-role-rename.mjs +125 -0
  235. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  236. package/engine/scripts/plugin-composition.json +15 -0
  237. package/engine/scripts/poc-mcp-host.mjs +455 -0
  238. package/engine/scripts/poll-fulfillment.sh +293 -0
  239. package/engine/scripts/poll-invoice.sh +74 -0
  240. package/engine/scripts/profile-doctor.mjs +686 -0
  241. package/engine/scripts/provider-select.sh +70 -0
  242. package/engine/scripts/provider-switch.sh +146 -0
  243. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  244. package/engine/scripts/python-runtime.sh +78 -0
  245. package/engine/scripts/query_runner.py +310 -0
  246. package/engine/scripts/release-argent.sh +574 -0
  247. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  248. package/engine/scripts/run-tests.sh +256 -0
  249. package/engine/scripts/self-sign-license.mjs +179 -0
  250. package/engine/scripts/session-health-scan.mjs +63 -0
  251. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  252. package/engine/scripts/setup.sh +22 -0
  253. package/engine/scripts/skill-install.mjs +629 -0
  254. package/engine/scripts/sku_translate.py +148 -0
  255. package/engine/scripts/sync-plugins.sh +90 -0
  256. package/engine/scripts/sync-presets.sh +27 -0
  257. package/engine/scripts/sync-scripts.sh +445 -0
  258. package/engine/scripts/telemetry.py +873 -0
  259. package/engine/scripts/ui-copy-count.mjs +220 -0
  260. package/engine/scripts/usage-anchor.sh +136 -0
  261. package/engine/scripts/usage_report.py +531 -0
  262. package/engine/scripts/validate-preset-schema.mjs +208 -0
  263. package/engine/scripts/vertical-init.mjs +323 -0
  264. package/engine/scripts/vertical-lint.mjs +482 -0
  265. package/engine/scripts/wenyao-browser.sh +73 -0
  266. package/engine/scripts/wenyao-fixture-export.py +116 -0
  267. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  268. package/engine/scripts/wenyao-login.sh +34 -0
  269. package/engine/scripts/wenyao-template-map.json +292 -0
  270. package/engine/scripts/wenyao_attach.py +131 -0
  271. package/engine/scripts/wenyao_download_template.py +144 -0
  272. package/engine/scripts/wenyao_fill_template.py +568 -0
  273. package/engine/scripts/wenyao_login.py +145 -0
  274. package/engine/scripts/wenyao_portal.py +633 -0
  275. package/engine/scripts/wenyao_recon.py +230 -0
  276. package/engine/scripts/wenyao_run.py +503 -0
  277. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  278. package/engine/scripts/wenyao_skill_contract.py +156 -0
  279. package/engine/scripts/wenyao_template_fetch.py +175 -0
  280. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  281. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  282. package/engine/scripts/ziniao-keepalive.sh +216 -0
  283. package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
  284. package/engine/scripts/ziniao-windows.sh +153 -0
  285. package/engine/version.mjs +1 -0
  286. package/engine/verticals/README.md +103 -0
  287. package/engine/verticals/_engine-words.baseline.txt +237 -0
  288. package/engine/verticals/_engine-words.json +19 -0
  289. package/engine/verticals/mercadolibre/README.md +43 -0
  290. package/engine/verticals/mercadolibre/config.json +709 -0
  291. package/engine/verticals/mercadolibre/flows.json +706 -0
  292. package/engine/verticals/mercadolibre/manifest.json +538 -0
  293. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  294. package/engine/verticals/mercadolibre/preflight.py +143 -0
  295. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  296. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  297. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  298. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  299. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  300. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  301. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  302. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  303. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  304. package/package.json +21 -4
  305. package/README.md +0 -3
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * vertical-lint —— 业务线门禁(P1,2026-09-29)
4
+ *
5
+ * 为什么需要它:注册表拆分后,"完全解耦"不能靠人记规矩 —— 必须由门禁保证:
6
+ * ① **命名空间**:flow id / 角色 / 技能 / env / 暂停开关 / 定时器 label / 派发 preset
7
+ * 一律带业务线前缀(否则两条业务线同机必然撞名);
8
+ * ② **跨线引用为零**:`verticals/A` 不得引用 `verticals/B` 的任何文件、flow、角色、技能;
9
+ * ③ **引擎不掺业务**:`scripts/` 与 `plugins/<plugin>/src/` 下不得出现业务词(新引入即失败,
10
+ * 历史存量用基线文件记账并逐阶段收敛 —— 见 `verticals/_engine-words.baseline.txt`);
11
+ * ④ **manifest 自洽**:必需字段齐、`requires` 有版本区间、声明的角色/技能真实存在。
12
+ *
13
+ * 合并/冲突判据**不在这里**(那是 `scripts/flow_registry.py` 的唯一职责)——本脚本
14
+ * 通过它的 `--json` 拿合并结果,避免"两处各判一套"(历史上 entitlements 三处各判就漂移过)。
15
+ *
16
+ * 用法:
17
+ * node scripts/vertical-lint.mjs [--root <dir>] [--json] [--update-baseline]
18
+ * 退出码:0 通过(可有 warning)/1 有 error
19
+ */
20
+ import { execFileSync } from 'node:child_process'
21
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
22
+ import { join, relative } from 'node:path'
23
+ import { fileURLToPath } from 'node:url'
24
+ import { dirname } from 'node:path'
25
+
26
+ const HERE = dirname(fileURLToPath(import.meta.url))
27
+ const DEFAULT_ROOT = join(HERE, '..')
28
+ const argOf = (name, dflt = null) => {
29
+ const i = process.argv.indexOf(name)
30
+ return i > -1 ? process.argv[i + 1] ?? dflt : dflt
31
+ }
32
+ const ROOT = argOf('--root', DEFAULT_ROOT)
33
+ const JSON_OUT = process.argv.includes('--json')
34
+ const UPDATE_BASELINE = process.argv.includes('--update-baseline')
35
+
36
+ const errors = []
37
+ const warnings = []
38
+ const err = (rule, msg) => errors.push({ rule, msg })
39
+ const warn = (rule, msg) => warnings.push({ rule, msg })
40
+
41
+ // ── 0. 合并注册表(冲突由加载器判定)──────────────────────────────
42
+ function mergedRegistry() {
43
+ const out = execFileSync('python3', [join(HERE, 'flow_registry.py'), '--json'], { // W3 修:--root 只换注册表根,脚本恒在 lint 自身目录(否则夹具根下没有该脚本 ⇒ ENOENT 假红)
44
+ env: { ...process.env, ARGENT_REGISTRY_ROOT: ROOT },
45
+ encoding: 'utf8',
46
+ maxBuffer: 32 * 1024 * 1024,
47
+ })
48
+ return JSON.parse(out)
49
+ }
50
+
51
+ // 已实现的 source kind(P4 端口表,唯一真源:`flow_source.py --list-sources`)——
52
+ // 引擎侧的声明必须与实现对齐,否则业务线会在运行到一半时才发现"这个数据面没人实现"。
53
+ let SOURCE_KINDS = []
54
+ try {
55
+ const raw = execFileSync('python3', [join(HERE, 'flow_source.py'), '--list-sources'], { encoding: 'utf8' })
56
+ SOURCE_KINDS = Object.keys(JSON.parse(raw))
57
+ } catch { /* 加载器/源表不可用时跳过这条判据(其它判据照跑) */ }
58
+
59
+ const verticalsRoot = join(ROOT, 'verticals')
60
+ const listVerticals = () => {
61
+ if (!existsSync(verticalsRoot)) return []
62
+ return readdirSync(verticalsRoot)
63
+ .filter((d) => !d.startsWith('.') && !d.startsWith('_'))
64
+ .filter((d) => {
65
+ try { return statSync(join(verticalsRoot, d)).isDirectory() } catch { return false }
66
+ })
67
+ .filter((d) => existsSync(join(verticalsRoot, d, 'manifest.json')) || existsSync(join(verticalsRoot, d, 'flows.json')))
68
+ .sort()
69
+ }
70
+
71
+ const VIDS = listVerticals()
72
+ let doc
73
+ try {
74
+ doc = mergedRegistry()
75
+ } catch (e) {
76
+ err('registry', `注册表合并失败:${(e.stderr || e.message || '').toString().trim().split('\n').slice(-3).join(' ')}`)
77
+ doc = { flows: {}, capabilities: {}, verticals: {} }
78
+ }
79
+
80
+ const canonicalFlows = doc.flows || {}
81
+ const capabilities = Object.keys(doc.capabilities || {}).filter((k) => !k.startsWith('_'))
82
+ const NAMESPACE = []
83
+ /** P1-A:声明了 `trigger.schedule.enabled=true` 的流程(试点期约束用;见下方全局判据)。 */
84
+ const SCHEDULE_ENABLED = []
85
+
86
+ // ── 1. 逐业务线检查 ──────────────────────────────────────────────
87
+ for (const vid of VIDS) {
88
+ const dir = join(verticalsRoot, vid)
89
+ const man = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'))
90
+ const fdoc = JSON.parse(readFileSync(join(dir, 'flows.json'), 'utf8'))
91
+ const at = (k) => `verticals/${vid}/${k}`
92
+ // 迁移期豁免清单(roles/skills):见下方流程循环处的说明
93
+ const legacyNames = new Set(man.legacyNaming || [])
94
+ const legacyRoles = legacyNames.has('roles')
95
+
96
+ // 1.1 manifest 自洽
97
+ for (const k of ['version', 'title', 'requires', 'ports', 'capabilities']) {
98
+ if (!man[k]) err('manifest', `${at('manifest.json')} 缺必需字段 \`${k}\``)
99
+ }
100
+ const req = man.requires || {}
101
+ for (const k of ['argent', 'dsh']) {
102
+ const v = String(req[k] ?? '')
103
+ if (!/^[<>=~^0-9][^|]*$/.test(v) || v.trim() === '') {
104
+ err('manifest', `${at('manifest.json')} requires.${k} 必须是版本区间(如 ">=0.2 <0.3"),当前 ${JSON.stringify(req[k])}`)
105
+ }
106
+ }
107
+ if (!man.verifiedOn || !Object.keys(man.verifiedOn || {}).length) {
108
+ warn('manifest', `${at('manifest.json')} 缺 verifiedOn("已在哪些版本验证过")—— 装机时无法判断该线是否可用于目标版本`)
109
+ }
110
+ const KNOWN_PORTS = ['feishu-base', 'http-json', 'sql', 'file', 'manual', 'feishu', 'web', 'wecom', 'feishu-card', 'log', 'env', 'none', 'internal-http', 'portal']
111
+ for (const [name, impl] of Object.entries(man.ports || {})) {
112
+ if (name.startsWith('_')) continue // `_note` 之类的说明键不参与判据(JSON 没有注释)
113
+ if (!KNOWN_PORTS.includes(String(impl))) {
114
+ warn('ports', `${at('manifest.json')} ports.${name}=${JSON.stringify(impl)} 不在已知端口实现表内(新增实现时同步更新 lint 的 KNOWN_PORTS)`)
115
+ }
116
+ }
117
+ for (const f of ['billing.json', 'preflight.py']) {
118
+ if (man[f.replace('.json', '')] && !existsSync(join(dir, f))) {
119
+ err('manifest', `${at('manifest.json')} 声明了 \`${f}\` 但文件不存在`)
120
+ }
121
+ }
122
+
123
+ // 1.2 声明的角色/技能必须存在且带前缀
124
+ for (const role of man.roles || []) {
125
+ if (!role.startsWith(`argent-${vid}-`)) {
126
+ if (legacyRoles) {
127
+ warn('namespace', `${at('manifest.json')} 角色 \`${role}\` 仍是旧名(manifest.legacyNaming 含 roles,P2b 退休)`)
128
+ } else {
129
+ err('namespace', `${at('manifest.json')} 角色 \`${role}\` 必须形如 \`argent-${vid}-<role>\``)
130
+ }
131
+ }
132
+ if (!existsSync(join(ROOT, 'presets', role))) {
133
+ err('manifest', `${at('manifest.json')} 声明的角色目录不存在:presets/${role}/`)
134
+ }
135
+ }
136
+ for (const sk of man.skills || []) {
137
+ if (!sk.startsWith(`${vid}-`)) {
138
+ if (legacyNames.has('skills')) {
139
+ warn('namespace', `${at('manifest.json')} 技能 \`${sk}\` 仍是旧名(manifest.legacyNaming 含 skills,P5 随重装窗口退休)`)
140
+ } else {
141
+ err('namespace', `${at('manifest.json')} 技能 \`${sk}\` 必须形如 \`${vid}-<skill>\``)
142
+ }
143
+ }
144
+ if (!existsSync(join(ROOT, 'skills', sk, 'SKILL.md'))) {
145
+ err('manifest', `${at('manifest.json')} 声明的技能不存在:skills/${sk}/SKILL.md`)
146
+ }
147
+ }
148
+
149
+ // 1.3 流程条目:命名空间与落位
150
+ const localFlows = Object.keys(fdoc.flows || {})
151
+ if (man.flows && man.flows.length && new Set(man.flows).size !== man.flows.length) {
152
+ err('manifest', `${at('manifest.json')} flows 列表有重复项`)
153
+ }
154
+ for (const local of localFlows) {
155
+ const id = `${vid}.${local}`
156
+ const flow = canonicalFlows[id]
157
+ if (!flow) {
158
+ err('registry', `流程 \`${id}\` 未出现在合并结果里(flows.json 与 manifest 不一致?)`)
159
+ continue
160
+ }
161
+ const role = String(flow.role || '')
162
+ if (!role.startsWith(`${vid}-`)) {
163
+ if (legacyRoles) warn('namespace', `${id}: role=\`${role}\` 仍是旧名(manifest.legacyNaming 含 roles,P2b 退休)`)
164
+ else err('namespace', `${id}: role=\`${role}\` 必须形如 \`${vid}-<role>\``)
165
+ }
166
+ // 缺省规则与 flow_config._derive 一致:未声明 dispatch.preset 时取 `argent-<role>`
167
+ const preset = String(flow.trigger?.dispatch?.preset || (role ? `argent-${role}` : ''))
168
+ if (!preset.startsWith(`argent-${vid}-`)) {
169
+ if (legacyRoles) warn('namespace', `${id}: preset=\`${preset}\` 仍是旧名(manifest.legacyNaming 含 roles,P2b 退休)`)
170
+ else err('namespace', `${id}: trigger.dispatch.preset=\`${preset}\` 必须形如 \`argent-${vid}-<role>\``)
171
+ }
172
+ const pause = String(flow.trigger?.pauseSwitch || '')
173
+ if (!pause.includes(`verticals/${vid}/`)) {
174
+ err('namespace', `${id}: trigger.pauseSwitch=\`${pause}\` 必须落在 \`verticals/${vid}/\` 命名空间内(两条线同机才不撞)`)
175
+ }
176
+ const promptTpl = String(flow.trigger?.promptTemplate || '')
177
+ if (!promptTpl.startsWith(`verticals/${vid}/`)) {
178
+ err('namespace', `${id}: trigger.promptTemplate=\`${promptTpl}\` 必须形如 \`verticals/${vid}/prompts/<flow>.md\``)
179
+ } else if (!existsSync(join(ROOT, promptTpl))) {
180
+ err('manifest', `${id}: 提示词模板不存在:${promptTpl}`)
181
+ }
182
+ const envFile = String(flow.source?.envFile || '')
183
+ // 只有**声明了** envFile 才校验落位:有的流程凭据由执行器自己加载(如日报读 daily-report.env),
184
+ // 注册表里没有这一项 —— 那不是违规,是还没把凭据搬到注册表(记为待办)。
185
+ if (envFile && !envFile.startsWith(`verticals/${vid}/`)) {
186
+ err('namespace', `${id}: source.envFile=\`${envFile}\` 必须落在 \`verticals/${vid}/env/\` 内`)
187
+ }
188
+ if (!envFile) {
189
+ warn('namespace', `${id}: 未声明 source.envFile(凭据由执行器自行加载?建议搬到 \`verticals/${vid}/env/\` 并在注册表声明)`)
190
+ }
191
+ const logf = String(flow.trigger?.log || '')
192
+ if (logf && !logf.includes(vid)) {
193
+ err('namespace', `${id}: trigger.log=\`${logf}\` 必须带业务线标识(否则两条线的日志会互相覆盖)`)
194
+ }
195
+ // P4:数据面必须在已实现表内(缺省 feishu-base = 历史行为)
196
+ const kind = String(flow.source?.kind || 'feishu-base')
197
+ if (SOURCE_KINDS.length && !SOURCE_KINDS.includes(kind)) {
198
+ err('ports', `${id}: source.kind=\`${kind}\` 未实现(已实现:${SOURCE_KINDS.join(', ')})—— 新数据面要么先实现,要么别声明`)
199
+ }
200
+ if (flow.capability && !capabilities.includes(String(flow.capability))) {
201
+ err('registry', `${id}: capability=\`${flow.capability}\` 未在注册表 capabilities 中定义`)
202
+ }
203
+ // D2(2026-10-03):「流程 → 技能目录」的**单一真源**是 `flow.skill` —— `argent sop` 只认它,
204
+ // 不再用 role 拼路径(角色名 ≠ 技能名:ml-invoice-wy ≠ ml-invoice)。缺声明 / 指向未声明的技能
205
+ // ⇒ 这里就拦住(否则只有运行时 `sop` 才响亮失败,装配期看不出来)。
206
+ const skill = String(flow.skill || '')
207
+ if (!skill) {
208
+ err('manifest', `${id}: 未声明 skill(流程→技能目录的单一真源;\`argent sop\` 缺它即报错)`
209
+ + `—— 请在 ${at('flows.json')} 该流程里加 "skill": "<技能名>"`)
210
+ } else {
211
+ if (!(man.skills || []).includes(skill)) {
212
+ err('manifest', `${id}: skill=\`${skill}\` 不在 ${at('manifest.json')}#skills 里(技能名必须由 manifest 声明)`)
213
+ }
214
+ if (!existsSync(join(ROOT, 'skills', skill, 'SKILL.md'))) {
215
+ err('manifest', `${id}: 技能不存在:skills/${skill}/SKILL.md`)
216
+ }
217
+ }
218
+ if (flow.integration) {
219
+ // P2a:适配器契约(`scripts/integration_contract.py` 的 INTEGRATIONS_DIR)尚未端口化,
220
+ // 因此**两处都认**:业务线内 `verticals/<id>/adapters/<name>/`(目标形态)或
221
+ // 引擎侧 `scripts/integrations/<name>/`(历史形态,P4 搬迁)。两处都没有才告警。
222
+ const inVertical = join(ROOT, 'verticals', vid, 'adapters', String(flow.integration))
223
+ const inEngine = join(ROOT, 'scripts', 'integrations', String(flow.integration))
224
+ if (!existsSync(inVertical) && !existsSync(inEngine)) {
225
+ warn('adapters', `${id}: integration=\`${flow.integration}\` 两处都不存在:${relative(ROOT, inVertical)}/ 或 ${relative(ROOT, inEngine)}/`)
226
+ }
227
+ }
228
+ if (String(flow.trigger?.mode) === 'real' && !(flow.guards?.writeFields || []).length) {
229
+ warn('guards', `${id}: mode=real 但没有声明 guards.writeFields(写路径白名单)—— 确认这是有意的`)
230
+ }
231
+ // ── 1.4 宿主排程声明(P1-A,2026-10-12)──────────────────────────────────
232
+ // 判据与运行期读取点(`scripts/flow_config.py` 的 schedule 段)**同一套**:这里是资产门禁,
233
+ // 那里是唯一读取点 —— 两处各判一次不是"第二份真源",而是"声明期拦住 + 运行期不静默"。
234
+ // 为什么要有:`trigger.schedule` 是**排程器唯一会读**的一段,写错了没有第二个人会喊
235
+ // (宿主投递失败只写它自己的日志、不重试)⇒ 必须在装配期就拦住。
236
+ const sch = flow.trigger?.schedule
237
+ if (sch !== undefined) {
238
+ const atS = `${id}: trigger.schedule`
239
+ if (!sch || typeof sch !== 'object' || Array.isArray(sch)) {
240
+ err('schedule', `${atS} 必须是对象`)
241
+ } else {
242
+ // ① 键白名单(`_` 前缀的说明键不参与判据;JSON 没有注释)
243
+ const ALLOWED = ['enabled', 'kind', 'time', 'timeZone', 'weekdays', 'expression', 'at',
244
+ 'everySecondsFrom', 'taskTitle', 'promptFile', 'onPause']
245
+ for (const k of Object.keys(sch)) {
246
+ if (k.startsWith('_') || ALLOWED.includes(k)) continue
247
+ err('schedule', `${atS} 出现未登记键 \`${k}\`(白名单:${ALLOWED.join(' / ')};`
248
+ + '排程段只描述"排程器怎么叫醒",⛔ 不许放业务规则/字段名/阈值/失败分类/白名单 —— 那属 L0/L1)')
249
+ }
250
+ // ② 选择器自洽
251
+ const KIND = String(sch.kind || '')
252
+ const KIND_OK = ['every', 'daily', 'weekly', 'cron', 'at']
253
+ const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?$/
254
+ if (!KIND_OK.includes(KIND)) err('schedule', `${atS}.kind=${JSON.stringify(sch.kind)} 不在 ${KIND_OK.join('|')} 内`)
255
+ if (typeof sch.enabled !== 'boolean') err('schedule', `${atS}.enabled 必须是布尔(当前 ${JSON.stringify(sch.enabled)})`)
256
+ if (KIND === 'every') {
257
+ if (String(sch.everySecondsFrom || '') !== 'intervalSec') {
258
+ err('schedule', `${atS}: kind=every 必须写 "everySecondsFrom": "intervalSec"(周期只有一份真源:trigger.intervalSec)`)
259
+ }
260
+ const iv = Number(flow.trigger?.intervalSec)
261
+ if (!Number.isFinite(iv)) err('schedule', `${atS}: kind=every 但 trigger.intervalSec 缺失 ⇒ 没有周期可用`)
262
+ else if (iv < 60) err('schedule', `${atS}: trigger.intervalSec=${iv} < 60(宿主 every_seconds 硬下限 60)`)
263
+ }
264
+ if (['daily', 'weekly', 'cron'].includes(KIND)) {
265
+ if (!String(sch.timeZone || '')) {
266
+ err('schedule', `${atS}: kind=${KIND} 必须显式给 timeZone(IANA,如 Asia/Shanghai)—— 宿主按它算本地钟表时间`)
267
+ } else {
268
+ try { new Intl.DateTimeFormat('en-US', { timeZone: String(sch.timeZone) }) }
269
+ catch { err('schedule', `${atS}.timeZone=${JSON.stringify(sch.timeZone)} 不是合法 IANA 时区`) }
270
+ }
271
+ }
272
+ if (['daily', 'weekly'].includes(KIND) && !TIME_RE.test(String(sch.time || ''))) {
273
+ err('schedule', `${atS}.time=${JSON.stringify(sch.time)} 不是 \`HH:mm:ss\`(宿主只认本地钟表时间)`)
274
+ }
275
+ if (KIND === 'weekly') {
276
+ const wd = sch.weekdays
277
+ if (!Array.isArray(wd) || !wd.length || wd.some((d) => !Number.isInteger(d) || d < 1 || d > 7)) {
278
+ err('schedule', `${atS}: kind=weekly 的 weekdays 必须是 1..7 的非空整数数组(ISO 周一=1)`)
279
+ }
280
+ }
281
+ if (KIND === 'cron') {
282
+ const ex = String(sch.expression || '')
283
+ const fields = ex.trim().split(/\s+/).filter(Boolean)
284
+ if (fields.length !== 5) err('schedule', `${atS}: kind=cron 的 expression 必须是**五字段** Vixie(当前 ${fields.length} 段)`)
285
+ if (/[LW#]|\b(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|MON|TUE|WED|THU|FRI|SAT|SUN)\b|^@/.test(ex)) {
286
+ err('schedule', `${atS}: cron 表达式用了宿主**明确拒绝**的方言(L/W/#、月份/星期英文名、@宏)`)
287
+ }
288
+ }
289
+ if (KIND === 'at') {
290
+ const v = String(sch.at || '')
291
+ if (!v || !Number.isFinite(Date.parse(v))) err('schedule', `${atS}: kind=at 必须给可解析的绝对时点 \`at\`(带显式时区)`)
292
+ }
293
+ // ③ 静态指令(宿主 prompt 创建时固定 ⇒ 必须是"一条 L1 命令",⛔ 不能带票号占位符)
294
+ const title = String(sch.taskTitle || '')
295
+ if (!title.trim()) err('schedule', `${atS} 缺 taskTitle(宿主必填)`)
296
+ else if (title !== title.trim()) err('schedule', `${atS}.taskTitle 首尾有空白(宿主拒)`)
297
+ else if (title.length > 120) err('schedule', `${atS}.taskTitle 超长(${title.length} > 120,宿主硬上限)`)
298
+ const pf = String(sch.promptFile || '')
299
+ if (!pf) {
300
+ err('schedule', `${atS} 缺 promptFile(静态轮次指令;⛔ 与 trigger.promptTemplate 是**两份不同的东西**)`)
301
+ } else {
302
+ const abs = pf.startsWith('verticals/') ? join(ROOT, pf) : join(dir, pf)
303
+ if (!existsSync(abs)) err('schedule', `${atS}.promptFile 指向的文件不存在:${relative(ROOT, abs)}`)
304
+ else {
305
+ const body = readFileSync(abs, 'utf8')
306
+ if (/\{\{\s*[A-Za-z_][\w.-]*\s*\}\}/.test(body)) {
307
+ err('schedule', `${atS}.promptFile 含模板占位符(双花括号包住的**名字**,如票号类)—— `
308
+ + '宿主在**创建任务时**就把 prompt 固定了,占位符不会被替换 ⇒ 静态指令里 ⛔ 不许出现模板(那是 trigger.promptTemplate 的职责)')
309
+ }
310
+ }
311
+ }
312
+ const onPause = String(sch.onPause || '')
313
+ if (!['deactivate', 'prompt-guard'].includes(onPause)) {
314
+ err('schedule', `${atS}.onPause=${JSON.stringify(sch.onPause)} 不在 deactivate|prompt-guard 内`)
315
+ }
316
+ // ④ 试点期约束(P1-A:**只对日报试点**):全局最多一条 enabled=true
317
+ if (sch.enabled === true) {
318
+ SCHEDULE_ENABLED.push({ id, taskTitle: title, promptFile: pf })
319
+ for (const need of ['kind', 'taskTitle', 'promptFile']) {
320
+ if (!sch[need]) err('schedule', `${atS}: enabled=true 必须同时给 \`${need}\``)
321
+ }
322
+ }
323
+ }
324
+ }
325
+ NAMESPACE.push({ id, vid, pause, preset, role, timerLabel: `com.whyshu.flow-trigger.${vid}.${local}` })
326
+ }
327
+
328
+ if (!existsSync(join(dir, 'tests'))) {
329
+ warn('scaffold', `${at('')} 缺 tests/ 目录(该线自己的测试与夹具)`)
330
+ }
331
+ }
332
+
333
+ // ── 1.5 宿主排程的**全局**判据(P1-A 试点期)────────────────────
334
+ // 为什么是全局而不是逐流程:宿主那侧只有**一张**任务表,`taskTitle` 是"任务 ↔ 流程"的对应键
335
+ // (`scripts/argent-schedule.mjs` 按它匹配);两条流程同名 = 匹配歧义,两条流程同时启用 =
336
+ // 试点期一次动两条生产流程(本批明确只批一条)。推广到多流程时改这一条(并在设计文档里登记)。
337
+ if (SCHEDULE_ENABLED.length > 1) {
338
+ err('schedule', `宿主排程试点期只允许**一条**流程启用(enabled=true),当前 ${SCHEDULE_ENABLED.length} 条:`
339
+ + SCHEDULE_ENABLED.map((s) => s.id).join('、')
340
+ + '—— 推广(P2/P3 逐流程)时改这条判据 + 在设计文档登记,⛔ 不要顺手放开')
341
+ }
342
+ const seenTitle = new Map()
343
+ for (const s of SCHEDULE_ENABLED) {
344
+ if (!s.taskTitle) continue
345
+ if (seenTitle.has(s.taskTitle)) err('schedule', `taskTitle 冲突:\`${s.taskTitle}\` 同时被 ${seenTitle.get(s.taskTitle)} 与 ${s.id} 使用(宿主只有一张任务表,标题是匹配键)`)
346
+ else seenTitle.set(s.taskTitle, s.id)
347
+ }
348
+
349
+ // ── 2. 跨业务线撞名(命名空间之外的东西必须唯一)──────────────────
350
+ const collisions = (key) => {
351
+ const seen = new Map()
352
+ for (const n of NAMESPACE) {
353
+ const v = n[key]
354
+ if (!v) continue
355
+ if (seen.has(v)) err('collision', `${key} 冲突:\`${v}\` 同时被 ${seen.get(v)} 与 ${n.id} 使用`)
356
+ else seen.set(v, n.id)
357
+ }
358
+ }
359
+ collisions('pause')
360
+ collisions('preset')
361
+ collisions('role')
362
+ collisions('timerLabel')
363
+
364
+ for (const vid of VIDS) {
365
+ const man = JSON.parse(readFileSync(join(verticalsRoot, vid, 'manifest.json'), 'utf8'))
366
+ for (const other of VIDS) {
367
+ if (other === vid) continue
368
+ const otherMan = JSON.parse(readFileSync(join(verticalsRoot, other, 'manifest.json'), 'utf8'))
369
+ for (const sk of man.skills || []) {
370
+ if ((otherMan.skills || []).includes(sk)) err('collision', `技能 \`${sk}\` 同时被 ${vid} 与 ${other} 声明`)
371
+ }
372
+ for (const role of man.roles || []) {
373
+ if ((otherMan.roles || []).includes(role)) err('collision', `角色 \`${role}\` 同时被 ${vid} 与 ${other} 声明`)
374
+ }
375
+ }
376
+ }
377
+
378
+ // ── 3. 跨线引用为零(扫该线目录下的所有文本文件)──────────────────
379
+ function walk(dir, out = []) {
380
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
381
+ if (e.name.startsWith('.')) continue
382
+ const p = join(dir, e.name)
383
+ if (e.isDirectory()) walk(p, out)
384
+ else if (/\.(mjs|js|py|json|md|sh|yml|yaml|txt)$/.test(e.name)) out.push(p)
385
+ }
386
+ return out
387
+ }
388
+ for (const vid of VIDS) {
389
+ const dir = join(verticalsRoot, vid)
390
+ const others = VIDS.filter((v) => v !== vid)
391
+ if (!others.length) continue
392
+ for (const f of walk(dir)) {
393
+ const text = readFileSync(f, 'utf8')
394
+ for (const other of others) {
395
+ if (text.includes(`verticals/${other}/`)) {
396
+ err('cross-ref', `${relative(ROOT, f)} 引用了别的业务线 \`${other}\`(跨线引用必须为零)`)
397
+ }
398
+ const otherMan = JSON.parse(readFileSync(join(verticalsRoot, other, 'manifest.json'), 'utf8'))
399
+ for (const token of [...(otherMan.roles || []), ...(otherMan.skills || []), ...(otherMan.flows || [])]) {
400
+ if (token && text.includes(token)) {
401
+ err('cross-ref', `${relative(ROOT, f)} 出现别的业务线的标识 \`${token}\`(属于 ${other})`)
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ // ── 4. 引擎不掺业务(基线记账,只拦**新增**)──────────────────────
409
+ const wordsFile = join(verticalsRoot, '_engine-words.json')
410
+ const baselineFile = join(verticalsRoot, '_engine-words.baseline.txt')
411
+ const ENGINE_GLOBS = ['scripts', 'plugins']
412
+ function engineFiles() {
413
+ const out = []
414
+ for (const g of ENGINE_GLOBS) {
415
+ const base = join(ROOT, g)
416
+ if (!existsSync(base)) continue
417
+ for (const f of walk(base)) {
418
+ const rel = relative(ROOT, f)
419
+ if (rel.startsWith('scripts/tests/')) continue // 测试可以引用业务夹具
420
+ if (rel.startsWith(`verticals/`)) continue
421
+ if (/\.(md|txt)$/.test(rel)) continue // 文档/说明不算引擎代码
422
+ // 生成产物:`capability-packs.lock.json` 记录**随包发布的文件路径**(含各业务线的提示词),
423
+ // 出现业务线名是正常现象;它由 --stamp 生成、无人手写,纳入扫描只会造成假红。
424
+ if (rel === 'scripts/capability-packs.lock.json') continue
425
+ // 生成产物(W3,0.6.0):**智能体 bundle** 的 `package.json` 由 `scripts/gen-agent-bundles.mjs`
426
+ // 从 **L0 货架声明**(`agents/catalog.json`)**逐字投影**而来 —— 它必然带业务线词:
427
+ // ① agent id(货架上的**商品名**本身就带);② 包名**必须** == `@whyshu/<目录名>`(门禁 P6)
428
+ // ⇒ 目录名里必然带那个词;③ 能力包名 / 凭据槽落点 / 集成清单(同样是 L0 声明的投影)。
429
+ // 它**零业务逻辑**(纯 bundle:零 JS,只被宿主读 `dsh.bundle`/`dependencies`/`argent.agent`),
430
+ // 与上面那条 lock 同类(生成产物、无人手写)⇒ 纳入扫描只会造成假红。
431
+ // ⛔ 判据没有放宽:业务词的**真源面** `agents/catalog.json` 不在扫描面内(`scripts/` + `plugins/` 之外),
432
+ // 而"货架声明 ⟺ 6 个产物"由 `scripts/tests/agent-bundle-generation.test.mjs` **逐字节**钉住。
433
+ if (/^plugins\/argent-agent-[^/]+\/package\.json$/.test(rel)) continue
434
+ out.push(f)
435
+ }
436
+ }
437
+ return out
438
+ }
439
+ if (existsSync(wordsFile)) {
440
+ const words = JSON.parse(readFileSync(wordsFile, 'utf8')).words || []
441
+ const counts = new Map()
442
+ for (const f of engineFiles()) {
443
+ const rel = relative(ROOT, f)
444
+ const text = readFileSync(f, 'utf8')
445
+ for (const w of words) {
446
+ const n = text.split(w).length - 1
447
+ if (n > 0) counts.set(`${rel}\t${w}`, n)
448
+ }
449
+ }
450
+ const lines = [...counts.entries()].map(([k, n]) => `${k}\t${n}`).sort()
451
+ if (UPDATE_BASELINE) {
452
+ writeFileSync(baselineFile, lines.join('\n') + (lines.length ? '\n' : ''))
453
+ console.log(` ✓ 已更新引擎业务词基线:${relative(ROOT, baselineFile)}(${lines.length} 条)`)
454
+ } else {
455
+ const base = new Map()
456
+ if (existsSync(baselineFile)) {
457
+ for (const line of readFileSync(baselineFile, 'utf8').split('\n')) {
458
+ if (!line.trim()) continue
459
+ const [file, w, n] = line.split('\t')
460
+ base.set(`${file}\t${w}`, Number(n))
461
+ }
462
+ }
463
+ for (const [k, n] of counts) {
464
+ const b = base.get(k) ?? 0
465
+ if (n > b) {
466
+ const [file, w] = k.split('\t')
467
+ err('engine-words', `引擎文件 ${file} 新出现业务词 \`${w}\`(基线 ${b} → 现在 ${n})—— 业务实现应落在 verticals/<id>/ 内`)
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ // ── 5. 输出 ─────────────────────────────────────────────────────
474
+ if (JSON_OUT) {
475
+ console.log(JSON.stringify({ verticals: VIDS, flows: NAMESPACE.length, errors, warnings }, null, 2))
476
+ } else {
477
+ console.log(`vertical-lint:业务线 ${VIDS.length} 条(${VIDS.join(', ') || '无'})、命名空间化流程 ${NAMESPACE.length} 条`)
478
+ for (const w of warnings) console.log(` ⚠️ [${w.rule}] ${w.msg}`)
479
+ for (const e of errors) console.log(` ❌ [${e.rule}] ${e.msg}`)
480
+ console.log(`\n结果:${errors.length ? `❌ ${errors.length} 个 error` : '✅ 通过'}(warn ${warnings.length})`)
481
+ }
482
+ process.exit(errors.length ? 1 : 0)
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ # wenyao-browser.sh — 长驻文鳐门户浏览器(登录态可复用,供人工过一次验证 + 自动化复用)
4
+ #
5
+ # 用法:
6
+ # bash scripts/wenyao-browser.sh start # 用独立 profile 启动 Chrome(CDP 9333,窗口常驻)+ 预填账号密码
7
+ # bash scripts/wenyao-browser.sh status # CDP 是否在 + 是否已登录 + 页面顶部文案
8
+ # bash scripts/wenyao-browser.sh entries # 打印页面入口文本(找《上海文鳐发票模版》下载入口)
9
+ # bash scripts/wenyao-browser.sh shot # 截图到 ~/.dsh/argent/wenyao-probe/current.png
10
+ # bash scripts/wenyao-browser.sh stop # 关闭这个实例(只杀本 profile 的 Chrome)
11
+ #
12
+ # 为什么长驻:门户登录是**阿里云行为验证码(滑块)**,自动化过不了(实测 2 次失败,且反复试会触发风控)。
13
+ # 做法:人工过一次 → 登录态留在浏览器与 profile(~/.dsh/argent/wenyao-profile)里,可重复使用;
14
+ # 后续自动化用 `connect_over_cdp http://127.0.0.1:9333` **接到同一个浏览器**(不新开),你还能看着它操作。
15
+ # 只读侦察:不导入订单、不导出文件、不改对方任何业务数据。
16
+
17
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ DATA="$HOME/.dsh/argent"
19
+ PROFILE="$DATA/wenyao-profile"
20
+ PORT="${WENYAO_CDP_PORT:-9333}"
21
+ CHROME="${WENYAO_CHROME:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}"
22
+ PROBE="$DATA/wenyao-probe"
23
+ PY="${WENYAO_PY:-$HOME/.dsh/argent/venv/bin/python3}"
24
+ [ -x "$PY" ] || PY="$(command -v python3 || true)"
25
+ URL="$( [ -f "$DATA/wenyao.env" ] && grep -E '^WENYAO_URL=' "$DATA/wenyao.env" | cut -d= -f2- || echo https://my.ffl-tech.com )"
26
+
27
+ cdp_up() { curl -s --max-time 3 "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; }
28
+
29
+ case "${1:-start}" in
30
+ start)
31
+ if [ ! -x "$CHROME" ]; then echo "[wenyao-browser] 找不到 Chrome:$CHROME" >&2; exit 1; fi
32
+ if [ ! -f "$DATA/wenyao.env" ]; then
33
+ echo "[wenyao-browser] 缺凭据 ~/.dsh/argent/wenyao.env(WENYAO_URL/WENYAO_PHONE/WENYAO_PASSWORD,600)" >&2; exit 1
34
+ fi
35
+ mkdir -p "$PROFILE" "$PROBE"; chmod 700 "$PROFILE"
36
+ if cdp_up; then
37
+ echo "[wenyao-browser] 已有长驻实例在跑(CDP ${PORT})"
38
+ else
39
+ echo "[wenyao-browser] 启动长驻 Chrome(独立 profile + CDP ${PORT})…"
40
+ open -na "Google Chrome" --args \
41
+ --user-data-dir="$PROFILE" --remote-debugging-port="$PORT" \
42
+ --no-first-run --no-default-browser-check --disable-features=Translate \
43
+ "$URL" >/dev/null 2>&1 || true
44
+ for i in $(seq 1 30); do cdp_up && break; sleep 1; done
45
+ cdp_up || { echo "[wenyao-browser] CDP 未就绪(${PORT});Chrome 可能被策略阻止" >&2; exit 1; }
46
+ fi
47
+ # 预填账号密码(让你只需滑滑块)
48
+ "$PY" "$HERE/wenyao_attach.py" --prefill || true
49
+ echo
50
+ echo "👉 请在弹出的 Chrome 窗口里:① 把滑块拖到最右边 ② 点「登录」"
51
+ echo " 过完验证后跑:bash $0 status (或让我来看)"
52
+ echo " 窗口会一直开着;登录态存在 ${PROFILE}(可重复使用)"
53
+ ;;
54
+ status)
55
+ if ! cdp_up; then echo "[wenyao-browser] CDP 未就绪(${PORT}):先跑 bash $0 start" >&2; exit 1; fi
56
+ "$PY" "$HERE/wenyao_attach.py" --status
57
+ ;;
58
+ entries)
59
+ if ! cdp_up; then echo "[wenyao-browser] CDP 未就绪(${PORT}):先跑 bash $0 start" >&2; exit 1; fi
60
+ "$PY" "$HERE/wenyao_attach.py" --status --entries
61
+ ;;
62
+ shot)
63
+ if ! cdp_up; then echo "[wenyao-browser] CDP 未就绪(${PORT}):先跑 bash $0 start" >&2; exit 1; fi
64
+ mkdir -p "$PROBE"
65
+ "$PY" "$HERE/wenyao_attach.py" --shot "$PROBE/current.png"
66
+ echo "截图:$PROBE/current.png"
67
+ ;;
68
+ stop)
69
+ pkill -f "user-data-dir=$PROFILE" 2>/dev/null && echo "[wenyao-browser] 已停止长驻实例" || echo "[wenyao-browser] 无运行中的实例"
70
+ ;;
71
+ *)
72
+ echo "用法: bash $0 {start|status|entries|shot|stop}" >&2; exit 2;;
73
+ esac