@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.
- package/MANIFEST.json +307 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +575 -0
- package/engine/scripts/billing-mode-daily.sh +12 -0
- package/engine/scripts/build-engine-package.mjs +123 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +377 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +134 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +46 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +260 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +720 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +86 -0
- package/engine/scripts/flow-timers.sh +182 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1024 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2275 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +194 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent_datadir.py +262 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +293 -0
- package/engine/scripts/poll-invoice.sh +74 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +70 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/run-tests.sh +256 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +136 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +73 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +34 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +216 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
- package/engine/scripts/ziniao-windows.sh +153 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +706 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +21 -4
- package/README.md +0 -3
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SOP / 判据**一致性门禁**(P4,2026-09-17)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么需要它:今天两次事故都不是"写错逻辑",而是**同一件事在两处各写了一份**:
|
|
6
|
+
* ① 引擎上传箱唛时勾 `产品箱唛`(写死在 orchestrator 的字段映射里),而写入守卫的白名单
|
|
7
|
+
* 来自注册表 `fields` —— 注册表没声明该字段 ⇒ 合法写入被守卫拒了(用户发现"附件传了但勾没打");
|
|
8
|
+
* ② 失败分类在两处(orchestrator._is_network_error 与 flow_failure 关键词表)⇒ 同一段错误文本
|
|
9
|
+
* 两个层给出不同结论。
|
|
10
|
+
* 判据散落本身可以接受(分层是有意为之),但**副本之间不能悄悄漂移**。本门禁就是那道锁:
|
|
11
|
+
* 任何一处漂移 ⇒ 退出码 1。
|
|
12
|
+
*
|
|
13
|
+
* 检查项:
|
|
14
|
+
* C1 引擎实际写入的字段 ⊇ 注册表声明(守卫白名单)—— 防"合法写入被守卫拒"
|
|
15
|
+
* C2 提示词里提到的相位 ∈ 引擎实际产出的相位 —— 防"提示词教角色做不存在的相位"
|
|
16
|
+
* C3 失败分类金样例(固定错误文本 → 期望分类)—— 防分类规则被无意改动
|
|
17
|
+
* C4 每流程提示词模板存在,且占位符都在渲染变量表里 —— 防生产里残留 `{{…}}`
|
|
18
|
+
* C5 引擎步骤集合 vs SKILL 记录的步骤(不一致只告警,供人工复核)
|
|
19
|
+
*
|
|
20
|
+
* 用法:node scripts/check-sop-consistency.mjs [--json] 退出码 0=一致 / 1=有漂移
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process'
|
|
23
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
24
|
+
import { join, resolve, dirname } from 'node:path'
|
|
25
|
+
import { loadRegistryDoc, resolveFlowId } from './lib/registry.mjs'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
import { resolveDataDir } from './lib/argent-datadir.mjs'
|
|
28
|
+
import { isDirectRun } from './lib/is-direct-run.mjs'
|
|
29
|
+
|
|
30
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
export const REPO = resolve(here, '..')
|
|
32
|
+
const SCRIPTS = join(REPO, 'scripts')
|
|
33
|
+
const PY = (process.env.ARGENT_PY || process.env.ARGENT_PYTHON || `${process.env.DSH_HOME || `${process.env.HOME}/.dsh`}/argent/venv/bin/python3`)
|
|
34
|
+
|
|
35
|
+
const read = (p) => readFileSync(p, 'utf8')
|
|
36
|
+
const uniq = (a) => [...new Set(a)]
|
|
37
|
+
|
|
38
|
+
/** 域内字段键(引擎用它们映射"逻辑角色 → 真列名")。 */
|
|
39
|
+
export const DOMAIN_KEYS = ['status', 'ready', 'store', 'step', 'note', 'shipment',
|
|
40
|
+
'labelFile', 'boxLabelFile', 'labelCheck', 'boxCheck', 'label_attach', 'box_attach',
|
|
41
|
+
'label_check', 'box_check']
|
|
42
|
+
|
|
43
|
+
/** C1 用:从 orchestrator 源码里取"引擎会写入的字段名"(映射字面量)。 */
|
|
44
|
+
export function engineFields(orchestratorSrc) {
|
|
45
|
+
// 只取**中文列名**:字段映射的值都是中文列名,而 `"status": "success"` 这类是结果枚举,
|
|
46
|
+
// 混进来会淹掉真信号(第一版就是这样,报了 9 个假阳性)。
|
|
47
|
+
// 另排除 `store`:引擎只**读**店名列(用途是匹配紫鸟店铺),不写它。
|
|
48
|
+
const out = new Set()
|
|
49
|
+
const keys = DOMAIN_KEYS.filter((k) => k !== 'store')
|
|
50
|
+
const re = new RegExp(`"(${keys.join('|')})"\\s*:\\s*"([^"]*[\\u4e00-\\u9fff][^"]*)"`, 'g')
|
|
51
|
+
for (const m of orchestratorSrc.matchAll(re)) out.add(m[2])
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** C1 用:注册表里声明的字段名(= 写入守卫白名单的来源)。 */
|
|
56
|
+
export function declaredFields(registryDoc) {
|
|
57
|
+
const out = new Set()
|
|
58
|
+
for (const spec of Object.values(registryDoc.flows || {})) {
|
|
59
|
+
for (const v of Object.values((spec.source || {}).variants || {})) {
|
|
60
|
+
for (const val of Object.values(v.fields || {})) if (typeof val === 'string' && val) out.add(val)
|
|
61
|
+
}
|
|
62
|
+
for (const val of Object.values((spec.source || {}).fields || {})) if (typeof val === 'string' && val) out.add(val)
|
|
63
|
+
}
|
|
64
|
+
out.add('货件号') // 票号列:脚本与守卫都按名字用
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 跑一个 Python 探针并取回它打印的 JSON(stdout 允许有噪声:优先取最后一个 `__JSON__` 标记行)。
|
|
69
|
+
* **拿不到就返回 `{__error}`**,由调用方决定"响亮"还是"跳过"——⛔ 不允许静默当成空结果。 */
|
|
70
|
+
export function runPyJson(file, args = []) {
|
|
71
|
+
try {
|
|
72
|
+
const out = execFileSync(PY, [file, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
73
|
+
const lines = out.split('\n').filter(Boolean)
|
|
74
|
+
const marked = lines.filter((l) => l.startsWith('__JSON__')).pop()
|
|
75
|
+
const raw = marked ? marked.slice('__JSON__'.length) : lines.pop() || ''
|
|
76
|
+
return JSON.parse(raw)
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { __error: (String(e.message || e) + ' | stderr: ' + String(e.stderr || '').trim().split('\n').pop()).slice(0, 300) }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** C2 用:**问引擎**它实际产出哪些相位(数据源 = 引擎自己的声明,见下)。
|
|
83
|
+
* `flow_source.py --list-phases` 是既有 CLI 入口(与 `--list-sources` 同规矩),
|
|
84
|
+
* 它转达的是 `flow_sources/common.py#PHASES`。⛔ 不要在门禁里解析引擎源码文本。 */
|
|
85
|
+
export function enginePhasesProbe() {
|
|
86
|
+
return runPyJson(join(SCRIPTS, 'flow_source.py'), ['--list-phases'])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** C2 用:引擎实际产出的相位。
|
|
90
|
+
*
|
|
91
|
+
* **为什么是"问引擎"而不是解析源码**(2026-10-08 修根因):
|
|
92
|
+
* 第一版只读 `scripts/flow_source.py` 的文本,用正则捞 `_phase'?]? = '…'`;而**相位赋值**
|
|
93
|
+
* 在 P4(2026-09-29)已搬进 `scripts/flow_sources/common.py`(`apply_pick()`)⇒ 那个文件里
|
|
94
|
+
* `_phase` 出现 **0 次** ⇒ 本函数恒返回**空集合** ⇒ C2 退化成"空集 ⊆ 任何集合"的
|
|
95
|
+
* **静默空转**(门禁输出里 `引擎产出()`):今天不红只是因为历史提示词目录已空,
|
|
96
|
+
* 一旦扫描面扩到 `verticals/<id>/prompts/`,真实提示词里的 identify/execute/await_release/
|
|
97
|
+
* needs_human 会**立刻变成 4 个假红**。更糟的是它第二行还**写死**了一份相位名单 ——
|
|
98
|
+
* 那是"判据副本"本身(引擎改名它照样通过)。
|
|
99
|
+
* ⇒ 现在只有一个真源:引擎的 `PHASES`(`apply_pick()` 打 `_phase` 时只从它取),
|
|
100
|
+
* 经既有 CLI 转达;本函数只负责"问 + 校验形状"。`probe` 可注入(回归夹具用)。
|
|
101
|
+
* ⛔ 空集/读不到 ⇒ **抛错**(由 C2 判红),绝不返回空集让判据静默失效。 */
|
|
102
|
+
export function producedPhases(probe = enginePhasesProbe) {
|
|
103
|
+
const raw = probe()
|
|
104
|
+
if (raw && raw.__error) throw new Error(`读不到引擎相位(--list-phases 执行失败):${raw.__error}`)
|
|
105
|
+
const list = Array.isArray(raw) ? raw : (raw || {}).phases
|
|
106
|
+
if (!Array.isArray(list) || !list.length) throw new Error(`--list-phases 没给出相位:${JSON.stringify(raw).slice(0, 160)}`)
|
|
107
|
+
return new Set(list.map((x) => String(x)))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** C2 的**核心相位**(金丝雀锚点):这几个相位一旦从引擎侧消失,C2 必须先响亮、再谈其它 ——
|
|
111
|
+
* 否则"相位集合"塌成小集合时门禁会跟着一起塌(同第一版的空集空转)。 */
|
|
112
|
+
export const CORE_PHASES = ['identify', 'execute', 'await_release', 'needs_human']
|
|
113
|
+
|
|
114
|
+
/** C2 判据核(**纯函数**,便于夹具/金丝雀两向回归):提示词声明的相位 ∉ 引擎产出 ⇒ 逐条列出。
|
|
115
|
+
* `templates` = `[{file, src}]`;`produced` = 相位集合(Set 或数组)。 */
|
|
116
|
+
export function promptPhaseBogus(produced, templates) {
|
|
117
|
+
const set = produced instanceof Set ? produced : new Set(produced || [])
|
|
118
|
+
const bogus = new Set()
|
|
119
|
+
for (const t of templates) {
|
|
120
|
+
for (const ph of mentionedPhases(t.src)) {
|
|
121
|
+
if (!set.has(ph) && !KNOWN_EVENTS.has(ph)) bogus.add(`${ph}@${t.file}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [...bogus]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** C2 用:台账事件名(提示词里合法出现,但**不是**相位)—— 用于把 C2 的误报排除掉。 */
|
|
128
|
+
export const KNOWN_EVENTS = new Set(['done', 'failed', 'blocked', 'planned', 'dispatched', 'wake',
|
|
129
|
+
'retry', 'failed_ack', 'phase', 'policy_apply', 'policy_new'])
|
|
130
|
+
|
|
131
|
+
/** C2 用:提示词里**声明为相位**的清单项(形如 `- \`identify\` —— …`)。
|
|
132
|
+
* 只认这种"清单式声明",不去抓正文里出现过的任何小写词 —— 第一版用白名单正则,
|
|
133
|
+
* 结果永远抓不到"引擎不产出的相位"(因为它只匹配已知名字),等于没检查。 */
|
|
134
|
+
export function mentionedPhases(promptSrc) {
|
|
135
|
+
const out = new Set()
|
|
136
|
+
for (const m of promptSrc.matchAll(/^\s*-\s*`([a-z_]{3,24})`\s*(?:——|—|-)/gm)) out.add(m[1])
|
|
137
|
+
return out
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** C2 用:历史提示词目录(`scripts/flow-prompts/`)里的模板文件名。
|
|
141
|
+
*
|
|
142
|
+
* **为什么容忍缺失**(2026-10-08 修):该目录是 **P2a 之前**的提示词位置 —— commit `48016b8`
|
|
143
|
+
* 已把 8 份模板全部迁到 `verticals/<id>/prompts/`,注册表 `trigger.promptTemplate` 是**唯一出处**。
|
|
144
|
+
* 迁移把 `scripts/flow-prompts/` 下的跟踪文件全删了 ⇒ 该目录**没有任何被跟踪的内容**,
|
|
145
|
+
* 而 **git 不跟踪空目录** ⇒ `git archive` 与 `git clone` **都不会产生它**。
|
|
146
|
+
* 于是真·新克隆上 `readdirSync` 抛 `ENOENT` ⇒ 本条用例判红(`run-tests.sh` 的"镜像空目录"
|
|
147
|
+
* 只是把目录补回来规避假红,**不等于**用例已健壮)。
|
|
148
|
+
* 目录不存在 = 「历史位置已废弃」,**不是判据漂移** ⇒ 按空集合处理。
|
|
149
|
+
*
|
|
150
|
+
* ⛔ 只吞 `ENOENT`:其它 IO 错误(EACCES 等)照旧抛出,不假装"目录是空的"。
|
|
151
|
+
* ⛔ **判据不放宽**:目录**存在**时逐份模板的断言与从前**完全一致**(下方 for 循环原样)。 */
|
|
152
|
+
export function legacyPromptFiles(dir = join(REPO, 'scripts', 'flow-prompts')) {
|
|
153
|
+
try {
|
|
154
|
+
return readdirSync(dir).filter((f) => f.endsWith('.md'))
|
|
155
|
+
} catch (e) {
|
|
156
|
+
if (e.code === 'ENOENT') return []
|
|
157
|
+
throw e
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** C4 用:模板占位符集合。 */
|
|
162
|
+
export const placeholders = (src) => uniq([...src.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]))
|
|
163
|
+
|
|
164
|
+
/** C4 用:wake 脚本提供的渲染变量。 */
|
|
165
|
+
export function renderVars(wakeSrc) {
|
|
166
|
+
const block = wakeSrc.slice(wakeSrc.indexOf('const vars = {'), wakeSrc.indexOf('const PROMPT ='))
|
|
167
|
+
return new Set([...block.matchAll(/^\s{2}(\w+):/gm)].map((m) => m[1]))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** C3 金样例:固定错误文本 → 期望分类(分类器是判据副本,必须锁住行为)。 */
|
|
171
|
+
export const GOLDEN = [
|
|
172
|
+
['失败:页面导航失败: Page.goto: Target page, context or browser has been closed', 'retryable'],
|
|
173
|
+
['超时 timeout waiting for selector', 'retryable'],
|
|
174
|
+
['预约失败:货件状态为「Reserva cancelada」且产品标签/箱唛均未上传,流程未完成', 'needs_agent'],
|
|
175
|
+
['缺 SKU:运营未填', 'needs_human'],
|
|
176
|
+
['紫鸟未授权该店铺环境', 'needs_human'],
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
export function classify(text) {
|
|
180
|
+
const out = execFileSync(PY, [join(REPO, 'scripts', 'flow_failure.py'), '--error', text, '--attempt', '1', '--json'],
|
|
181
|
+
{ encoding: 'utf8' })
|
|
182
|
+
return JSON.parse(out).kind
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
/** C7 用(纯函数,便于回归):检查"裁定沉淀"两边是否互相指得上。
|
|
187
|
+
* 规则:① 每条政策必须有 sourceCase 与 authorizer(可追溯红线);
|
|
188
|
+
* ② whyRef 形如 `<file>#<anchor>` 时,文件要存在且含该锚点;
|
|
189
|
+
* ③ 技能条目里引用的 `政策 \`pol-…\`` 必须真实存在于政策库。 */
|
|
190
|
+
export function learnLinkProblems(policies = [], { readFile = () => null, exists = () => false } = {}) {
|
|
191
|
+
const problems = []
|
|
192
|
+
const ids = new Set(policies.map((p) => p.id))
|
|
193
|
+
for (const p of policies) {
|
|
194
|
+
if (!p.sourceCase) problems.push(`政策 ${p.id} 缺 sourceCase(无法追溯来源案例)`)
|
|
195
|
+
if (!p.authorizer) problems.push(`政策 ${p.id} 缺 authorizer(不知道谁授权的)`)
|
|
196
|
+
const ref = String(p.whyRef || '')
|
|
197
|
+
if (ref.includes('#')) {
|
|
198
|
+
const [file, anchor] = ref.split('#')
|
|
199
|
+
if (!exists(file)) problems.push(`政策 ${p.id} 的 whyRef 指向不存在的文件:${file}`)
|
|
200
|
+
else {
|
|
201
|
+
const text = readFile(file) || ''
|
|
202
|
+
if (!text.includes(anchor)) problems.push(`政策 ${p.id} 的 whyRef 锚点 ${anchor} 在 ${file} 里找不到`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return problems
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** C7 用:扫描技能文档里对政策 id 的引用。 */
|
|
210
|
+
export function citedPolicyIds(markdown) {
|
|
211
|
+
return [...String(markdown).matchAll(/政策\s*`(pol-[0-9a-z]+)`/g)].map((m) => m[1])
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function run() {
|
|
215
|
+
const checks = []
|
|
216
|
+
const add = (id, level, title, detail = '') => checks.push({ id, level, title, detail })
|
|
217
|
+
|
|
218
|
+
// P2a:注册表经加载器合并(base + 业务线);别直读 base(会漏业务线流程)
|
|
219
|
+
const reg = loadRegistryDoc()
|
|
220
|
+
const orch = read(join(REPO, 'scripts', 'orchestrator.py'))
|
|
221
|
+
const wake = read(join(REPO, 'scripts', 'flow-trigger-wake.mjs'))
|
|
222
|
+
|
|
223
|
+
// C1 守卫白名单覆盖引擎写入字段
|
|
224
|
+
const decl = declaredFields(reg)
|
|
225
|
+
const eng = engineFields(orch)
|
|
226
|
+
const missing = [...eng].filter((f) => !decl.has(f))
|
|
227
|
+
if (missing.length) {
|
|
228
|
+
add('C1', 'fail', `引擎写入字段未在注册表声明:${missing.join('、')}`,
|
|
229
|
+
'守卫白名单来自注册表 ⇒ 这类写入会被静默拒绝(2026-09-17 产品箱唛就是如此)。修:把字段加进 flows.<id>.source.variants.<v>.fields')
|
|
230
|
+
} else {
|
|
231
|
+
add('C1', 'ok', `引擎写入的 ${eng.size} 个字段都在注册表声明内(守卫不会误拒)`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// C2 提示词相位 ⊆ 引擎产出相位
|
|
235
|
+
// 数据源 = **引擎自己的声明**(`flow_sources/common.py#PHASES`),经既有 CLI 转达;
|
|
236
|
+
// ⛔ 不再解析 `flow_source.py` 的源码文本(P4 搬走赋值后那次解析恒得空集 ⇒ C2 静默空转)。
|
|
237
|
+
let produced = new Set()
|
|
238
|
+
let producedErr = ''
|
|
239
|
+
try { produced = producedPhases() } catch (e) { producedErr = String(e.message || e) }
|
|
240
|
+
const coreMissing = CORE_PHASES.filter((p) => !produced.has(p))
|
|
241
|
+
// 历史目录可能**根本不存在**(git 不跟踪空目录 ⇒ 新克隆/裸检出没有它)—— 那不判红,见 legacyPromptFiles。
|
|
242
|
+
const LEGACY_PROMPTS = join(REPO, 'scripts', 'flow-prompts')
|
|
243
|
+
const prompts = legacyPromptFiles(LEGACY_PROMPTS)
|
|
244
|
+
const templates = prompts.map((f) => ({ file: f, src: read(join(LEGACY_PROMPTS, f)) }))
|
|
245
|
+
const bogus = promptPhaseBogus(produced, templates)
|
|
246
|
+
if (producedErr || !produced.size || coreMissing.length) {
|
|
247
|
+
// 数据源断了 / 相位集合塌了 ⇒ **响亮判红**:此时 bogus 全是假红,不能拿它当结论
|
|
248
|
+
add('C2', 'fail', `读不到引擎产出的相位(数据源断了):${producedErr || `集合=${[...produced].sort().join('/') || '(空)'}`
|
|
249
|
+
+ (coreMissing.length ? `,缺核心相位 ${coreMissing.join('、')}` : '')}`,
|
|
250
|
+
'C2 的判据是「提示词相位 ⊆ 引擎产出」⇒ 源侧塌成空集/小集合时它会**静默假绿**。'
|
|
251
|
+
+ '修:让 `flow_source.py --list-phases`(唯一真源 flow_sources/common.py#PHASES)能正常输出,'
|
|
252
|
+
+ '并确认 apply_pick() 仍在打这些相位')
|
|
253
|
+
} else if (bogus.length) {
|
|
254
|
+
add('C2', 'fail', `提示词提到引擎不会产出的相位:${bogus.join('、')}`, '改提示词或改引擎,二选一')
|
|
255
|
+
}
|
|
256
|
+
// 「扫了几份」写在 title 里(ok 的 detail 不打印)—— 免得 C2 空转时被误读成"8 份模板都验过了"
|
|
257
|
+
else add('C2', 'ok', `提示词相位集合 ⊆ 引擎产出(${[...produced].sort().join('/')})` + (prompts.length
|
|
258
|
+
? `;已扫历史目录 ${prompts.length} 份模板`
|
|
259
|
+
: ';`scripts/flow-prompts/` 不在(P2a 已迁到 verticals/<id>/prompts/)⇒ 按空集合通过,判据未放宽'))
|
|
260
|
+
|
|
261
|
+
// C3 分类金样例
|
|
262
|
+
const bad = []
|
|
263
|
+
for (const [text, want] of GOLDEN) {
|
|
264
|
+
let got = '(异常)'
|
|
265
|
+
try { got = classify(text) } catch (e) { got = '(执行失败)' }
|
|
266
|
+
if (got !== want) bad.push(`「${text.slice(0, 26)}…」期望 ${want} 实得 ${got}`)
|
|
267
|
+
}
|
|
268
|
+
if (bad.length) add('C3', 'fail', `失败分类与金样例不一致:${bad.join(';')}`, '分类是判据副本(政策/重跑/唤醒都依赖它)⇒ 改动必须同步更新金样例并说明理由')
|
|
269
|
+
else add('C3', 'ok', `失败分类金样例 ${GOLDEN.length} 条全部一致`)
|
|
270
|
+
|
|
271
|
+
// C4 模板存在 + 占位符有定义
|
|
272
|
+
const vars = renderVars(wake)
|
|
273
|
+
const probs = []
|
|
274
|
+
for (const [fid, spec] of Object.entries(reg.flows || {})) {
|
|
275
|
+
// 模板路径:注册表可声明 trigger.promptTemplate,否则按 flow_config 的缺省推导规则取 flow-prompts/<id>.md
|
|
276
|
+
const rel = (spec.trigger || {}).promptTemplate || `flow-prompts/${fid}.md`
|
|
277
|
+
// P2a:路径解析与 flow_config._resolve_prompt_path 同规矩 ——
|
|
278
|
+
// 业务线声明的路径是**仓库根相对**(verticals/<id>/prompts/…),历史缺省是 scripts/ 相对。
|
|
279
|
+
const tpl = existsSync(join(REPO, rel)) ? join(REPO, rel) : join(REPO, 'scripts', rel)
|
|
280
|
+
if (!existsSync(tpl)) { probs.push(`${fid}: 模板缺失`); continue }
|
|
281
|
+
for (const ph of placeholders(read(tpl))) if (!vars.has(ph)) probs.push(`${fid}: {{${ph}}} 无定义`)
|
|
282
|
+
const ftpl = tpl.replace(/\.md$/, '.failure.md')
|
|
283
|
+
if (!existsSync(ftpl)) probs.push(`${fid}: 失败模板缺失`)
|
|
284
|
+
}
|
|
285
|
+
if (probs.length) add('C4', 'fail', `提示词模板问题:${probs.join(';')}`, '占位符必须都能被 flow-trigger-wake.mjs 渲染,否则生产里会残留 {{…}}')
|
|
286
|
+
else add('C4', 'ok', `全部流程的派发/失败模板存在,占位符(${vars.size} 个变量)都有定义`)
|
|
287
|
+
|
|
288
|
+
// C5 引擎步骤 vs SKILL(不一致只告警)
|
|
289
|
+
const engSteps = uniq([...orch.matchAll(/步骤\s?(\d+)/g)].map((m) => Number(m[1])).filter((n) => n > 0)).sort((a, b) => a - b)
|
|
290
|
+
const skill = join(REPO, 'skills', 'ml-fulfillment-sop', 'SKILL.md')
|
|
291
|
+
const skillSteps = existsSync(skill)
|
|
292
|
+
? uniq([...read(skill).matchAll(/步骤\s?(\d+)/g)].map((m) => Number(m[1]))).sort((a, b) => a - b) : []
|
|
293
|
+
const onlyEng = engSteps.filter((n) => !skillSteps.includes(n))
|
|
294
|
+
if (!skillSteps.length) add('C5', 'warn', 'SKILL 里没找到步骤编号(无法比对)', 'skills/ml-fulfillment-sop/SKILL.md')
|
|
295
|
+
else if (onlyEng.length) add('C5', 'warn', `SKILL 缺步骤:${onlyEng.join('、')}(引擎有 ${engSteps.join('/')})`, '把新步骤补进 SKILL,或确认为内部子步骤')
|
|
296
|
+
else add('C5', 'ok', `SKILL 步骤覆盖引擎步骤(${engSteps.join('/')})`)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
// C6 判据副本:引擎/分类器的**生效规则**必须与注册表 `failureClasses` 逐字一致(P3)
|
|
300
|
+
const fc = reg.failureClasses || {}
|
|
301
|
+
// 不允许"静默跳过":拿不到就带上真实错误(Python 找不到 / 导入失败 / stdout 有噪声都要能看见)
|
|
302
|
+
// 探针实现 = 模块级 `runPyJson()`(与 C2 的 `--list-phases` 同一份实现)
|
|
303
|
+
const engRules = runPyJson(join(REPO, 'scripts', '_engine_rules_probe.py'))
|
|
304
|
+
const wantKeys = (fc.engineNetwork || {}).patterns || []
|
|
305
|
+
const wantTypes = (fc.engineNetwork || {}).errTypes || []
|
|
306
|
+
if (engRules && engRules.__error) add('C6', 'warn', `读不到引擎生效规则:${engRules.__error}`, `PY=${PY}(导入 orchestrator 失败时跳过)`)
|
|
307
|
+
else if (!engRules) add('C6', 'warn', '读不到引擎生效规则(空输出)', `PY=${PY}`)
|
|
308
|
+
else if (JSON.stringify(engRules[0]) !== JSON.stringify(wantKeys) || JSON.stringify(engRules[1]) !== JSON.stringify(wantTypes)) {
|
|
309
|
+
add('C6', 'fail', '引擎网络口径与注册表不一致',
|
|
310
|
+
`引擎=${JSON.stringify(engRules)}|注册表=${JSON.stringify([wantKeys, wantTypes])} ⇒ 改一处忘另一处正是今天事故的形态`)
|
|
311
|
+
} else add('C6', 'ok', `引擎网络口径与注册表一致(${wantKeys.length} 个关键词)`)
|
|
312
|
+
|
|
313
|
+
const ffPats = runPyJson(join(REPO, 'scripts', 'flow_failure.py'), ['--patterns'])
|
|
314
|
+
if (ffPats && ffPats.__error) add('C6b', 'warn', `读不到分类器生效规则:${ffPats.__error}`, `PY=${PY}`)
|
|
315
|
+
else if (!ffPats) add('C6b', 'warn', '读不到分类器生效规则(空输出)', `PY=${PY}`)
|
|
316
|
+
else {
|
|
317
|
+
const bad = []
|
|
318
|
+
if (JSON.stringify(ffPats.retryable) !== JSON.stringify((fc.flowRetryable || {}).patterns || [])) bad.push('retryable')
|
|
319
|
+
if (JSON.stringify(ffPats.needs_human) !== JSON.stringify((fc.needsHuman || {}).patterns || [])) bad.push('needs_human')
|
|
320
|
+
// 2026-10-10(仓容满批)**判据收紧**:分类器新增 `capacityFull` 组(排在 needs_human 之前,
|
|
321
|
+
// 否则仓容满的文案会被 `需运营` 吞进 needs_human,就再也分不出是哪条链)。既然注册表是
|
|
322
|
+
// 唯一权威,就必须逐字比 —— 只比两个老组等于给新组留了一个"代码里悄悄另写一份"的口子。
|
|
323
|
+
// 同批把既有的 `pageFlaky` 也纳入(此前**从未**被比过;现比值与注册表逐字一致,属收紧不属放松)。
|
|
324
|
+
if (JSON.stringify(ffPats.capacityFull) !== JSON.stringify((fc.capacityFull || {}).patterns || [])) bad.push('capacityFull')
|
|
325
|
+
if (JSON.stringify(ffPats.pageFlaky) !== JSON.stringify((fc.pageFlaky || {}).patterns || [])) bad.push('pageFlaky')
|
|
326
|
+
if (bad.length) add('C6b', 'fail', `分类器生效规则与注册表不一致:${bad.join('、')}`, '分类器已改为注册表优先 ⇒ 不一致说明代码里另有内置改动未回写注册表')
|
|
327
|
+
else add('C6b', 'ok', '分类器生效规则与注册表逐字一致(capacityFull / pageFlaky / retryable / needs_human)')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
// C7 裁定沉淀闭环:政策 ↔ 技能条目互相指得上(P6)
|
|
332
|
+
const dataDir = resolveDataDir({ env: process.env }).dir // 内核三级链(唯一判据)
|
|
333
|
+
const polFile = join(dataDir, 'flow-policy.jsonl')
|
|
334
|
+
let policies = []
|
|
335
|
+
if (existsSync(polFile)) {
|
|
336
|
+
policies = read(polFile).split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean)
|
|
337
|
+
}
|
|
338
|
+
const readAny = (f) => { try { return readFileSync(f, 'utf8') } catch { return null } }
|
|
339
|
+
const existsAny = (f) => { try { return existsSync(f) } catch { return false } }
|
|
340
|
+
const linkProblems = learnLinkProblems(policies, { readFile: readAny, exists: existsAny })
|
|
341
|
+
// 技能文档里引用的政策 id 必须存在
|
|
342
|
+
const skillDirs = join(REPO, 'skills')
|
|
343
|
+
if (existsSync(skillDirs)) {
|
|
344
|
+
for (const d of readdirSync(skillDirs)) {
|
|
345
|
+
const f = join(skillDirs, d, 'references', 'pitfalls.md')
|
|
346
|
+
if (!existsSync(f)) continue
|
|
347
|
+
for (const id of citedPolicyIds(read(f))) {
|
|
348
|
+
if (policies.length && !policies.some((p) => p.id === id)) linkProblems.push(`${d}/references/pitfalls.md 引用了不存在的政策 ${id}`)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (linkProblems.length) {
|
|
353
|
+
add('C7', 'fail', `裁定沉淀的两边对不上(${linkProblems.length} 处)`, linkProblems.slice(0, 3).join(';'))
|
|
354
|
+
} else {
|
|
355
|
+
add('C7', 'ok', `政策库 ${policies.length} 条:追溯字段齐、与技能条目的交叉引用可解析`)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
// C8 技能里的表 id 必须是注册表声明过的(2026-09-17:xzh 技能曾指向已作废的旧表 ⇒ 判据漂移)
|
|
360
|
+
// 2026-09-20(迭代1-步骤4):表 id 可能是**env 解析出来的**(`baseEnv/tableEnv`),
|
|
361
|
+
// 只看字面量会把"已声明但走 env"的表误判成未声明 ⇒ 改为"字面量 ∪ 解析结果"。
|
|
362
|
+
const declaredTables = new Set()
|
|
363
|
+
const collectFrom = (src) => {
|
|
364
|
+
if (!src || typeof src !== 'object') return
|
|
365
|
+
for (const k of ['table', 'base']) {
|
|
366
|
+
const v = src[k]
|
|
367
|
+
if (typeof v === 'string' && v) declaredTables.add(v)
|
|
368
|
+
}
|
|
369
|
+
for (const v of Object.values(src.variants || {})) collectFrom(v)
|
|
370
|
+
}
|
|
371
|
+
for (const spec of Object.values(reg.flows || {})) collectFrom(spec.source)
|
|
372
|
+
// 解析后的 id(best-effort:缺部署 env 时用 --lax 拿字面量,不因缺凭据让门禁假红)
|
|
373
|
+
try {
|
|
374
|
+
const raw = execFileSync(PY, [join(SCRIPTS, 'flow_config.py'), '--list', '--lax'], { encoding: 'utf8' })
|
|
375
|
+
for (const m of raw.matchAll(/\btbl[A-Za-z0-9]{10,}\b/g)) declaredTables.add(m[0])
|
|
376
|
+
} catch { /* 解析失败不致命:字面量集合仍然有效 */ }
|
|
377
|
+
// 另:解析成功时把 base/table 一并收集(--json 逐流程)
|
|
378
|
+
for (const fid of Object.keys(reg.flows || {})) {
|
|
379
|
+
try {
|
|
380
|
+
const cfg = JSON.parse(execFileSync(PY, [join(SCRIPTS, 'flow_config.py'), '--flow', fid, '--json'],
|
|
381
|
+
{ encoding: 'utf8' }))
|
|
382
|
+
const t = (cfg.source || {}).table
|
|
383
|
+
if (typeof t === 'string' && t) declaredTables.add(t)
|
|
384
|
+
} catch { /* 缺 env 的流程跳过 */ }
|
|
385
|
+
}
|
|
386
|
+
const skillsRoot = join(REPO, 'skills')
|
|
387
|
+
const strayTables = []
|
|
388
|
+
if (existsSync(skillsRoot)) {
|
|
389
|
+
for (const d of readdirSync(skillsRoot)) {
|
|
390
|
+
const dir = join(skillsRoot, d)
|
|
391
|
+
let files = []
|
|
392
|
+
try { files = readdirSync(dir, { withFileTypes: true }) } catch { continue }
|
|
393
|
+
for (const f of files) {
|
|
394
|
+
const fp = f.isDirectory() ? join(dir, f.name, 'pitfalls.md') : join(dir, f.name)
|
|
395
|
+
if (!f.isDirectory() && !f.name.endsWith('.md')) continue
|
|
396
|
+
if (!existsSync(fp)) continue
|
|
397
|
+
for (const line of read(fp).split('\n')) {
|
|
398
|
+
if (/作废|历史|旧|deprecated|retired/i.test(line)) continue
|
|
399
|
+
for (const m of line.matchAll(/tbl[A-Za-z0-9]{10,}/g)) {
|
|
400
|
+
if (!declaredTables.has(m[0])) strayTables.push(`${fp.split('/skills/')[1]}: ${m[0]}`)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (strayTables.length) {
|
|
407
|
+
add('C8', 'fail', `技能里引用了未在注册表声明的表:${[...new Set(strayTables)].join('、')}`,
|
|
408
|
+
'技能是角色的判读依据 ⇒ 指向已废弃的表会让角色照旧口径干活;改技能或改注册表')
|
|
409
|
+
} else {
|
|
410
|
+
add('C8', 'ok', `技能引用的表都在注册表声明内(${declaredTables.size} 张)`)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return checks
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const isMain = isDirectRun(import.meta.url)
|
|
417
|
+
if (isMain) {
|
|
418
|
+
const checks = run()
|
|
419
|
+
const fails = checks.filter((c) => c.level === 'fail')
|
|
420
|
+
if (process.argv.includes('--json')) {
|
|
421
|
+
console.log(JSON.stringify({ checks, summary: { fail: fails.length, ok: checks.filter((c) => c.level === 'ok').length } }, null, 2))
|
|
422
|
+
} else {
|
|
423
|
+
const icon = { ok: '✅', warn: '⚠️', fail: '❌' }
|
|
424
|
+
for (const c of checks) {
|
|
425
|
+
console.log(` ${icon[c.level]} [${c.id}] ${c.title}`)
|
|
426
|
+
if (c.level !== 'ok' && c.detail) console.log(` ↳ ${c.detail}`)
|
|
427
|
+
}
|
|
428
|
+
console.log(fails.length ? `\n❌ 一致性门禁未通过(${fails.length} 项)` : '\n✅ 一致性门禁通过(判据副本未漂移)')
|
|
429
|
+
}
|
|
430
|
+
process.exit(fails.length ? 1 : 0)
|
|
431
|
+
}
|