@whyshu/argent-engine 0.0.0-stage → 0.6.1
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 +341 -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 +576 -0
- package/engine/scripts/billing-mode-daily.sh +13 -0
- package/engine/scripts/build-engine-package.mjs +126 -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 +378 -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/collect-evidence.mjs +42 -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 +135 -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 +47 -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 +261 -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/final-check.mjs +43 -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 +735 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +87 -0
- package/engine/scripts/flow-timers.sh +184 -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 +1025 -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 +2278 -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 +195 -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.sh +18 -0
- package/engine/scripts/lib/argent_datadir.py +285 -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 +295 -0
- package/engine/scripts/poll-invoice.sh +76 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +71 -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/retire-inventory.mjs +40 -0
- package/engine/scripts/run-tests.sh +257 -0
- package/engine/scripts/s1-audit.mjs +85 -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 +137 -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 +74 -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 +35 -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 +217 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
- package/engine/scripts/ziniao-windows.sh +154 -0
- package/engine/skills/README.md +86 -0
- package/engine/skills/argent-dev/SKILL.md +41 -0
- package/engine/skills/internal.json +37 -0
- package/engine/skills/lktop-pm/SKILL.md +35 -0
- package/engine/skills/lktopapp-dev/SKILL.md +34 -0
- package/engine/skills/ml-config-guide/SKILL.md +50 -0
- package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
- package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
- package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
- package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
- package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
- package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
- package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
- package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
- package/engine/skills/ml-invoice/SKILL.md +506 -0
- package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
- package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
- package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
- package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
- package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
- package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
- package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
- package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
- package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
- package/engine/skills/pet-dev/SKILL.md +42 -0
- package/engine/skills/whyshu-pm/SKILL.md +35 -0
- package/engine/skills/whyshu-web-dev/SKILL.md +50 -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 +738 -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 +22 -4
- package/requirements.txt +5 -0
- package/README.md +0 -3
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-judgment-drift.mjs —— **判断面漂移**门禁(P0 0.1–0.4 的可跑形式;2026-10-03)
|
|
4
|
+
*
|
|
5
|
+
* 为什么有它:契约(`core/contracts/argent.contracts.json#judgments`)一旦和代码分家,
|
|
6
|
+
* "判断留在 CLI 里"就只剩一句口号 —— 而且**两种分家都是坏的**:
|
|
7
|
+
* · 契约声明了、代码里没有 ⇒ 宿主按契约调,拿到的是幻觉(假绿);
|
|
8
|
+
* · 代码里实现了、契约没声明 ⇒ 新判断**绕过了清单**,宿主不可能知道它存在(漏报)。
|
|
9
|
+
* 所以本门禁是**双向**的,且**清单与 token 全部从契约文件读**(门禁里不另写第二份判据,
|
|
10
|
+
* 否则门禁自己就变成第二个真源 —— 那正是 `scripts/check-sop-consistency.mjs:4-11` 记录过的事故)。
|
|
11
|
+
*
|
|
12
|
+
* ## R1 契约↔代码(0 容忍:drift 必须为 0)
|
|
13
|
+
* | 检查 | 方向 | 判据 |
|
|
14
|
+
* |---|---|---|
|
|
15
|
+
* | R1a | 契约 → 代码 | `argent.contracts.json#verbs` 的每个 verb 必须能被 CLI 枚举到(`--help --json`) |
|
|
16
|
+
* | R1b | 代码 → 契约 | CLI 枚举到的每个 verb / 组内子命令都必须在契约里声明 |
|
|
17
|
+
* | R1c | 契约 → 代码 | `judgments[*].implementations[*].at` 必须存在,且 anchor 落在 `[行号±ownerSlackLines]` 内 |
|
|
18
|
+
* | R1d | 代码 → 契约 | anchor 出现在扫描面(`judgmentMeta.scan`)里的**任何未声明文件** ⇒ 漂移 |
|
|
19
|
+
* | R1e | 契约 → 代码 | A2 适配层(`reasonScanGlobs`)里发出的 reason 短码必须在 `reasonCodes` 里 |
|
|
20
|
+
* | R1f | 契约自洽 | `argent.contracts.json#verbs` ↔ `verbs.schema.json#verbs` ↔ `scope` 三向逐键对齐 |
|
|
21
|
+
* | R1g | schema → MCP | `verbs.schema.json` 里标了 `surface:["mcp"]` 的 verb 必须有同名 MCP tool(`argent_<verb>`),反之亦然(P1 第二批加:`argent_preflight` 曾**没有 verb 块** ⇒ 这一向恒绿) |
|
|
22
|
+
* | R1h | 契约自洽 | 组 verb 的 `subcommands` 必须与 schema 里同名组的 `subcommands` **逐键对齐**(否则子命令可以只写在一边) |
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ R1a/R1b 的"代码侧"来自 `core/cli/argent.mjs --help --json`,它报的是**代码里真的实现了**的
|
|
25
|
+
* verb/子命令(P1 第二批改;此前直接照抄契约 ⇒ 这两向恒绿,属于假绿空间)。
|
|
26
|
+
*
|
|
27
|
+
* ## R2 SKILL/提示词**复述**判断(首日允许非零,只 warn;`--strict` 才红)
|
|
28
|
+
* §P0-A V1 / P0 0.4:判断常量**允许提及、禁止定义**——同一行命中 ≥2 个 `restateTokens`
|
|
29
|
+
* 且无引用点(`restateScan.citationTokens`)⇒ 记一条。基线见实施方案 §2.3 D5。
|
|
30
|
+
*
|
|
31
|
+
* ## 用法与退出码
|
|
32
|
+
* ```
|
|
33
|
+
* node scripts/check-judgment-drift.mjs # 只报告,**恒 exit 0**
|
|
34
|
+
* node scripts/check-judgment-drift.mjs --strict # R1 或 R2 有命中 ⇒ exit 1
|
|
35
|
+
* node scripts/check-judgment-drift.mjs --strict-contract # 只有 R1(契约↔代码)有命中才 exit 1
|
|
36
|
+
* node scripts/check-judgment-drift.mjs --json # 机器可读
|
|
37
|
+
* node scripts/check-judgment-drift.mjs --root <dir> # 换根(夹具自测用;仍然只读)
|
|
38
|
+
* ```
|
|
39
|
+
* ## 纪律
|
|
40
|
+
* · **只读**:不改文件、不写数据目录/台账、不联网;唯一子进程是 `node <CLI> --help --json`(枚举 verb)。
|
|
41
|
+
* · **默认不失败**:形态照 `scripts/check-docs-drift.mjs`(文档漂移常年存在,接进 run-tests 就是恒红陷阱)。
|
|
42
|
+
* 回归只覆盖 `scripts/tests/judgment-drift.test.mjs` 的**纯夹具**(夹具注入漂移 ⇒ 必红)。
|
|
43
|
+
*/
|
|
44
|
+
import { execFileSync } from 'node:child_process'
|
|
45
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
46
|
+
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
47
|
+
import { fileURLToPath } from 'node:url'
|
|
48
|
+
import { isDirectRun } from './lib/is-direct-run.mjs'
|
|
49
|
+
|
|
50
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
51
|
+
const SELF_ROOT = resolve(HERE, '..')
|
|
52
|
+
|
|
53
|
+
// ── 参数 ────────────────────────────────────────────────────────────────────
|
|
54
|
+
const argv = process.argv.slice(2)
|
|
55
|
+
const has = (f) => argv.includes(f)
|
|
56
|
+
const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined }
|
|
57
|
+
if (has('-h') || has('--help')) {
|
|
58
|
+
console.log(readFileSync(fileURLToPath(import.meta.url), 'utf8').split('*/')[0].replace(/^\/\*\*?/, '').trim())
|
|
59
|
+
process.exit(0)
|
|
60
|
+
}
|
|
61
|
+
const STRICT = has('--strict')
|
|
62
|
+
const STRICT_CONTRACT = has('--strict-contract')
|
|
63
|
+
const AS_JSON = has('--json')
|
|
64
|
+
const ROOT = resolve(val('--root') || SELF_ROOT)
|
|
65
|
+
|
|
66
|
+
// ── 契约(唯一真源)──────────────────────────────────────────────────────────
|
|
67
|
+
const CONTRACTS_REL = 'core/contracts/argent.contracts.json'
|
|
68
|
+
const SCHEMA_REL = 'core/contracts/verbs.schema.json'
|
|
69
|
+
const CLI_REL = 'core/cli/argent.mjs'
|
|
70
|
+
|
|
71
|
+
const contractsPath = join(ROOT, CONTRACTS_REL)
|
|
72
|
+
const schemaPath = join(ROOT, SCHEMA_REL)
|
|
73
|
+
|
|
74
|
+
/** 读文件(失败返回 null,由调用方转成 finding —— 不抛栈)。 */
|
|
75
|
+
function readText(p) {
|
|
76
|
+
try { return readFileSync(p, 'utf8') } catch { return null }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** glob → 正则(`*` = 段内任意;`**` = 跨段任意;**`**\/` = 零段或多段**,故 `scripts/**\/*.py` 也匹配
|
|
80
|
+
* `scripts/flow_case.py`)。与 kernel-purity 的 `.*` 略有差别(那条要求至少一段)—— 本门禁取更宽的语义,
|
|
81
|
+
* 宁可多扫,也不留"顶层脚本不在扫描面"的假绿空间。 */
|
|
82
|
+
export function globToRe(glob) {
|
|
83
|
+
const s = String(glob)
|
|
84
|
+
let re = ''
|
|
85
|
+
for (let i = 0; i < s.length; i++) {
|
|
86
|
+
const c = s[i]
|
|
87
|
+
if (c === '*') {
|
|
88
|
+
if (s[i + 1] === '*') {
|
|
89
|
+
if (s[i + 2] === '/') { re += '(?:.*/)?'; i += 2 } else { re += '.*'; i++ }
|
|
90
|
+
} else re += '[^/]*'
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
if ('\\^$.|?+()[]{}'.includes(c)) { re += `\\${c}`; continue }
|
|
94
|
+
re += c
|
|
95
|
+
}
|
|
96
|
+
return new RegExp(`^${re}$`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const toPosix = (p) => p.split(sep).join('/')
|
|
100
|
+
|
|
101
|
+
/** 递归列出 `dir` 下所有文件(仓库相对 POSIX 路径)。 */
|
|
102
|
+
export function walkFiles(dir, base = dir, out = []) {
|
|
103
|
+
let entries = []
|
|
104
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out }
|
|
105
|
+
for (const e of entries) {
|
|
106
|
+
if (e.name === '__pycache__' || e.name === '.git' || e.name === 'node_modules') continue
|
|
107
|
+
const p = join(dir, e.name)
|
|
108
|
+
let st = null
|
|
109
|
+
try { st = statSync(p) } catch { continue }
|
|
110
|
+
if (st.isDirectory()) walkFiles(p, base, out)
|
|
111
|
+
else if (st.isFile()) out.push(toPosix(relative(base, p)))
|
|
112
|
+
}
|
|
113
|
+
return out
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 按 globs(可带 excludeGlobs)筛出仓库相对文件清单。 */
|
|
117
|
+
export function selectFiles(root, globs, excludeGlobs = []) {
|
|
118
|
+
const inc = (globs || []).map(globToRe)
|
|
119
|
+
const exc = (excludeGlobs || []).map(globToRe)
|
|
120
|
+
const all = walkFiles(root)
|
|
121
|
+
return all.filter((f) => inc.some((r) => r.test(f)) && !exc.some((r) => r.test(f))).sort()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `file:line` → `{file, line}`;不合法 ⇒ null。 */
|
|
125
|
+
export function parseAt(at) {
|
|
126
|
+
const m = /^(.*):(\d+)$/.exec(String(at || ''))
|
|
127
|
+
if (!m) return null
|
|
128
|
+
return { file: m[1], line: Number(m[2]) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* R1:契约↔代码双向漂移(纯函数;`deps` 便于夹具注入)。
|
|
133
|
+
* @returns {{findings: object[], stats: object}}
|
|
134
|
+
*/
|
|
135
|
+
export function checkContractDrift({ root, contracts, schema, cliVerbs, cliError = null, mcpTools = null, mcpError = null, readFile = (p) => readText(p), files = null }) {
|
|
136
|
+
const findings = []
|
|
137
|
+
const push = (rule, message, extra = {}) => findings.push({ rule, message, ...extra })
|
|
138
|
+
const meta = contracts.judgmentMeta || {}
|
|
139
|
+
const slack = Number(meta.ownerSlackLines || 0)
|
|
140
|
+
const scan = meta.scan || { globs: [], excludeGlobs: [] }
|
|
141
|
+
const scanFiles = files || selectFiles(root, scan.globs, scan.excludeGlobs)
|
|
142
|
+
|
|
143
|
+
// ── R1a/R1b:verb 与子命令 ────────────────────────────────────────────────
|
|
144
|
+
const declared = Object.keys(contracts.verbs || {})
|
|
145
|
+
if (cliError) {
|
|
146
|
+
push('R1a', `无法从 CLI 枚举 verb(${CLI_REL} --help --json 失败):${cliError}`, { at: CLI_REL })
|
|
147
|
+
} else {
|
|
148
|
+
for (const v of declared) {
|
|
149
|
+
if (!cliVerbs.verbs.includes(v)) push('R1a', `契约声明了 verb \`${v}\`,CLI 里没有(假绿:宿主按契约调会拿到幻觉)`, { id: v, at: CLI_REL })
|
|
150
|
+
}
|
|
151
|
+
for (const v of cliVerbs.verbs) {
|
|
152
|
+
if (!declared.includes(v)) push('R1b', `CLI 里有 verb \`${v}\`,契约没声明(漏报:新判断绕过了清单)`, { id: v, at: CLI_REL })
|
|
153
|
+
}
|
|
154
|
+
for (const v of declared) {
|
|
155
|
+
const sub = (contracts.verbs[v] || {}).subcommands
|
|
156
|
+
if (!sub) continue
|
|
157
|
+
const actual = (cliVerbs.subcommands || {})[v] || []
|
|
158
|
+
for (const s of Object.keys(sub)) {
|
|
159
|
+
if (!actual.includes(s)) push('R1a', `契约声明了 \`${v} ${s}\`,CLI 里没有这个子命令`, { id: `${v} ${s}`, at: CLI_REL })
|
|
160
|
+
}
|
|
161
|
+
for (const s of actual) {
|
|
162
|
+
if (!Object.keys(sub).includes(s)) push('R1b', `CLI 里有子命令 \`${v} ${s}\`,契约没声明`, { id: `${v} ${s}`, at: CLI_REL })
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── R1g/R1h:schema ↔ MCP tools、组 verb 的 subcommands 逐键对齐(P1 第二批加)──
|
|
168
|
+
// R1g:`surface` 标了 mcp 的 verb ⇒ 必须有同名 tool;每个 tool(`argent_<verb>`)⇒ 必须有 verb 块。
|
|
169
|
+
// `mcpTools === null`(server 文件不在场 / 枚举失败)时**不判红**(夹具根没有 core/mcp/server.mjs;
|
|
170
|
+
// 真仓库里枚举失败会以 fatal 计 —— 见 run() 的 mcpError 分支)。
|
|
171
|
+
if (Array.isArray(mcpTools)) {
|
|
172
|
+
const toolOf = (v) => `argent_${v}`
|
|
173
|
+
const verbsMcp = Object.entries(schema.verbs || {}).filter(([, v]) => (v.surface || []).includes('mcp')).map(([k]) => k)
|
|
174
|
+
for (const v of verbsMcp) {
|
|
175
|
+
if (!mcpTools.includes(toolOf(v))) push('R1g', `schema 的 \`${v}\` 标了 mcp 面,但 MCP server 没有 tool \`${toolOf(v)}\`(标注说谎)`, { id: v, at: 'core/mcp/server.mjs' })
|
|
176
|
+
}
|
|
177
|
+
for (const t of mcpTools) {
|
|
178
|
+
const v = String(t).replace(/^argent_/, '')
|
|
179
|
+
if (!schema.verbs || !schema.verbs[v]) push('R1g', `MCP tool \`${t}\` 没有对应的 verb 块(**自洽检查会漏**:它绕过了判断面清单)`, { id: v, at: 'core/contracts/verbs.schema.json' })
|
|
180
|
+
}
|
|
181
|
+
} else if (mcpError) {
|
|
182
|
+
push('R1g', `无法枚举 MCP tools(core/mcp/server.mjs tools/list 失败):${mcpError}`, { at: 'core/mcp/server.mjs' })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// R1h:组 verb 的子命令:契约(唯一口径)与 schema(宿主读的那份)必须逐键一致。
|
|
186
|
+
// 只看**两边都声明了 subcommands** 的 verb —— 契约里没有 `subcommands` 的 verb 不管(它不是组)。
|
|
187
|
+
// 形状容忍:契约里 `subcommands` 是**对象**(子命令 → 说明),schema 里可以是**数组**(只列名字)。
|
|
188
|
+
const subNames = (v) => (Array.isArray(v) ? v.map(String) : (v && typeof v === 'object' ? Object.keys(v) : null))
|
|
189
|
+
for (const [v, c] of Object.entries(contracts.verbs || {})) {
|
|
190
|
+
const cSubs = c.subcommands ? subNames(c.subcommands) : null
|
|
191
|
+
if (!cSubs) continue
|
|
192
|
+
const sSubs = schema.verbs && schema.verbs[v] ? subNames(schema.verbs[v].subcommands) : null
|
|
193
|
+
if (!sSubs) { push('R1h', `契约的组 verb \`${v}\` 声明了子命令,schema 里却没有 subcommands(宿主读不到子命令清单)`, { id: v }); continue }
|
|
194
|
+
for (const s of cSubs) if (!sSubs.includes(s)) push('R1h', `契约声明了 \`${v} ${s}\`,schema 的 subcommands 里没有`, { id: `${v} ${s}` })
|
|
195
|
+
for (const s of sSubs) if (!cSubs.includes(s)) push('R1h', `schema 声明了 \`${v} ${s}\`,契约的 subcommands 里没有`, { id: `${v} ${s}` })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── R1c/R1d:judgment 实现位置与 anchor ──────────────────────────────────
|
|
199
|
+
const judgments = contracts.judgments || {}
|
|
200
|
+
let anchorChecked = 0
|
|
201
|
+
for (const [key, j] of Object.entries(judgments)) {
|
|
202
|
+
const impls = j.implementations || []
|
|
203
|
+
if (!impls.length) push('R1c', `判断 ${key} 没有 implementations(契约必须点名实现位置)`, { id: key })
|
|
204
|
+
const declaredFiles = new Set()
|
|
205
|
+
for (const im of impls) {
|
|
206
|
+
const at = parseAt(im.at)
|
|
207
|
+
if (!at) { push('R1c', `判断 ${key} 的 at 不是 file:line 形态:${im.at}`, { id: key }); continue }
|
|
208
|
+
declaredFiles.add(at.file)
|
|
209
|
+
const text = readFile(join(root, at.file))
|
|
210
|
+
if (text === null) { push('R1c', `判断 ${key} 的实现文件不存在:${at.file}`, { id: key, at: im.at }); continue }
|
|
211
|
+
const lines = text.split('\n')
|
|
212
|
+
if (at.line < 1 || at.line > lines.length) {
|
|
213
|
+
push('R1c', `判断 ${key} 的行号越界:${im.at}(文件只有 ${lines.length} 行)`, { id: key, at: im.at })
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
anchorChecked++
|
|
217
|
+
const lo = Math.max(1, at.line - slack)
|
|
218
|
+
const hi = Math.min(lines.length, at.line + slack)
|
|
219
|
+
const inWindow = lines.slice(lo - 1, hi).some((l) => l.includes(im.anchor))
|
|
220
|
+
if (inWindow) continue
|
|
221
|
+
const foundAt = lines.findIndex((l) => l.includes(im.anchor)) + 1
|
|
222
|
+
push('R1c', `判断 ${key} 的 anchor 不在声明位置附近:${im.at}(±${slack} 行内没有 ${JSON.stringify(im.anchor)}`
|
|
223
|
+
+ (foundAt ? `;该文件实际在第 ${foundAt} 行` : ';该文件里根本没有这个 anchor') + ')',
|
|
224
|
+
{ id: key, at: im.at, anchor: im.anchor })
|
|
225
|
+
}
|
|
226
|
+
if (j.owner && parseAt(j.owner) && impls.length && j.owner !== impls[0].at) {
|
|
227
|
+
push('R1c', `判断 ${key} 的 owner(${j.owner})必须等于 implementations[0].at(${impls[0].at})—— 契约自身不能有两份主实现位置`, { id: key })
|
|
228
|
+
}
|
|
229
|
+
const anchors = impls.map((i) => i.anchor).filter(Boolean)
|
|
230
|
+
for (const f of scanFiles) {
|
|
231
|
+
if (declaredFiles.has(f)) continue
|
|
232
|
+
const text = readFile(join(root, f))
|
|
233
|
+
if (text === null) continue
|
|
234
|
+
for (const a of anchors) {
|
|
235
|
+
if (text.includes(a)) {
|
|
236
|
+
push('R1d', `扫描面里出现未声明的实现位置:${f} 含判断 ${key} 的 anchor ${JSON.stringify(a)}`
|
|
237
|
+
+ '(要么把它写进 judgments 的 implementations,要么把 anchor 改精确)', { id: key, file: f, anchor: a })
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── R1e:reason 短码词表 ────────────────────────────────────────────────
|
|
245
|
+
const codes = new Set(Object.keys(contracts.reasonCodes || {}).filter((k) => !k.startsWith('_')))
|
|
246
|
+
for (const f of (meta.reasonScanGlobs || [])) {
|
|
247
|
+
const text = readFile(join(root, f))
|
|
248
|
+
if (text === null) { push('R1e', `reasonScanGlobs 里的文件不存在:${f}`, { at: f }); continue }
|
|
249
|
+
for (const m of text.matchAll(/reason:\s*'([a-z][a-z0-9_]*)'/g)) {
|
|
250
|
+
if (!codes.has(m[1])) push('R1e', `${f} 发出的 reason 短码 ${JSON.stringify(m[1])} 不在契约 reasonCodes 里(宿主无法分支)`, { file: f, anchor: m[1] })
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
findings,
|
|
256
|
+
stats: { verbs: declared.length, cliVerbs: (cliVerbs && cliVerbs.verbs || []).length, judgments: Object.keys(judgments).length, anchors: anchorChecked, scanFiles: scanFiles.length },
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* R2:SKILL/提示词复述判断常量(只 warn)。
|
|
262
|
+
* 判据 = 一行命中 ≥2 个 restateTokens 且该行**无引用点**。
|
|
263
|
+
*/
|
|
264
|
+
export function checkRestatement({ root, contracts, readFile = (p) => readText(p), files = null }) {
|
|
265
|
+
const meta = contracts.judgmentMeta || {}
|
|
266
|
+
const rs = meta.restateScan || { globs: [], excludeGlobs: [], citationTokens: [] }
|
|
267
|
+
const tokens = []
|
|
268
|
+
for (const [key, j] of Object.entries(contracts.judgments || {})) {
|
|
269
|
+
for (const t of j.restateTokens || []) tokens.push({ token: t, judgment: key })
|
|
270
|
+
}
|
|
271
|
+
const scanFiles = files || selectFiles(root, rs.globs, rs.excludeGlobs)
|
|
272
|
+
const minTokens = Number(rs.minTokens || 2) // 阈值也是契约数据(不写死在门禁里)
|
|
273
|
+
const out = []
|
|
274
|
+
if (!tokens.length) return { findings: out, stats: { files: scanFiles.length, tokens: 0 } }
|
|
275
|
+
for (const f of scanFiles) {
|
|
276
|
+
const text = readFile(join(root, f))
|
|
277
|
+
if (text === null) continue
|
|
278
|
+
text.split('\n').forEach((line, i) => {
|
|
279
|
+
const hit = [...new Set(tokens.filter((t) => line.includes(t.token)).map((t) => t.token))]
|
|
280
|
+
if (hit.length < minTokens) return
|
|
281
|
+
if ((rs.citationTokens || []).some((c) => line.includes(c))) return
|
|
282
|
+
out.push({ rule: 'R2', file: f, line: i + 1, tokens: hit, text: line.trim().slice(0, 160) })
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
return { findings: out, stats: { files: scanFiles.length, tokens: tokens.length } }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 枚举 MCP server 里**实际存在**的 tools(`tools/list`)。
|
|
290
|
+
* 与 `cliVerbsOf` 对称:门禁的"MCP 侧"来源必须是被检对象自己报的,而不是契约抄一遍。
|
|
291
|
+
* @returns {{ok:boolean, tools?:string[], skipped?:boolean, error?:string}}
|
|
292
|
+
* `skipped` = `core/mcp/server.mjs` 不在场(夹具根)⇒ 这一向不判(**不是**放行真仓库:真仓库里它必须在场)
|
|
293
|
+
*/
|
|
294
|
+
export function mcpToolsOf(root) {
|
|
295
|
+
const server = join(root, 'core', 'mcp', 'server.mjs')
|
|
296
|
+
if (!existsSync(server)) return { ok: false, skipped: true }
|
|
297
|
+
try {
|
|
298
|
+
const input = [
|
|
299
|
+
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05' } }),
|
|
300
|
+
JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }),
|
|
301
|
+
'',
|
|
302
|
+
].join('\n')
|
|
303
|
+
const out = execFileSync(process.execPath, [server], { encoding: 'utf8', input, timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
304
|
+
for (const line of String(out).split('\n')) {
|
|
305
|
+
const s = line.trim()
|
|
306
|
+
if (!s) continue
|
|
307
|
+
let m = null
|
|
308
|
+
try { m = JSON.parse(s) } catch { continue }
|
|
309
|
+
if (m && m.id === 2) {
|
|
310
|
+
const tools = m.result && Array.isArray(m.result.tools) ? m.result.tools.map((t) => String(t.name)) : null
|
|
311
|
+
if (tools) return { ok: true, tools }
|
|
312
|
+
if (m.error) return { ok: false, error: String(m.error.message || JSON.stringify(m.error)).slice(0, 200) }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { ok: false, error: 'tools/list 没有返回 tools 数组' }
|
|
316
|
+
} catch (e) {
|
|
317
|
+
return { ok: false, error: String((e && e.message) || e).slice(0, 200) }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** 枚举 CLI 里**实际存在**的 verb(`--help --json`;失败退化为文本解析)。 */
|
|
322
|
+
export function cliVerbsOf(root) {
|
|
323
|
+
const cli = join(root, CLI_REL)
|
|
324
|
+
if (!existsSync(cli)) return { ok: false, error: `${CLI_REL} 不存在` }
|
|
325
|
+
try {
|
|
326
|
+
const out = execFileSync(process.execPath, [cli, '--help', '--json'], { encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
327
|
+
const doc = JSON.parse(out.slice(out.indexOf('{')))
|
|
328
|
+
return { ok: true, verbs: (doc.verbs || []).map((v) => v.name), subcommands: Object.fromEntries((doc.verbs || []).filter((v) => v.subcommands).map((v) => [v.name, v.subcommands])) }
|
|
329
|
+
} catch (e) {
|
|
330
|
+
// 退化为文本解析(老版本 CLI 没有 --help --json):这是**兼容**,不是第二份判据(verb 仍由 CLI 自己报)
|
|
331
|
+
try {
|
|
332
|
+
const out = execFileSync(process.execPath, [cli, '--help'], { encoding: 'utf8', timeout: 30000 })
|
|
333
|
+
return { ok: true, verbs: [...out.matchAll(/^ {2}([a-z][a-z-]*)\s/gm)].map((m) => m[1]), subcommands: {} }
|
|
334
|
+
} catch (e2) {
|
|
335
|
+
return { ok: false, error: String((e2 && e2.message) || e.message) }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── 主流程 ──────────────────────────────────────────────────────────────────
|
|
341
|
+
export function run({ root = ROOT, strict = STRICT, strictContract = STRICT_CONTRACT } = {}) {
|
|
342
|
+
const contractsText = readText(join(root, CONTRACTS_REL))
|
|
343
|
+
const schemaText = readText(join(root, SCHEMA_REL))
|
|
344
|
+
if (contractsText === null || schemaText === null) {
|
|
345
|
+
return { ok: false, fatal: `契约文件缺失:${contractsText === null ? CONTRACTS_REL : SCHEMA_REL}`, drift: [], restate: [], stats: {} }
|
|
346
|
+
}
|
|
347
|
+
const contracts = JSON.parse(contractsText)
|
|
348
|
+
const schema = JSON.parse(schemaText)
|
|
349
|
+
|
|
350
|
+
// 契约自身自洽(这也是"契约有代码没有"的一类:schema 与 contracts 必须逐键对齐)
|
|
351
|
+
const selfFindings = []
|
|
352
|
+
const cVerbs = Object.keys(contracts.verbs || {})
|
|
353
|
+
const sVerbs = Object.keys(schema.verbs || {})
|
|
354
|
+
for (const v of cVerbs) if (!sVerbs.includes(v)) selfFindings.push({ rule: 'R1f', message: `argent.contracts.json 声明了 \`${v}\`,verbs.schema.json 里没有`, id: v })
|
|
355
|
+
for (const v of sVerbs) {
|
|
356
|
+
const isCli = ((schema.verbs[v] || {}).surface || []).includes('cli')
|
|
357
|
+
if (isCli && !cVerbs.includes(v)) selfFindings.push({ rule: 'R1f', message: `verbs.schema.json 的 \`${v}\` 标了 cli 面,argent.contracts.json 里没有`, id: v })
|
|
358
|
+
}
|
|
359
|
+
const scopeAll = [...((schema.scope || {}).local || []), ...((schema.scope || {}).account || [])]
|
|
360
|
+
for (const v of sVerbs) if (!scopeAll.includes(v)) selfFindings.push({ rule: 'R1f', message: `verbs.schema.json 的 \`${v}\` 没有 scope 归属`, id: v })
|
|
361
|
+
for (const v of scopeAll) if (!sVerbs.includes(v)) selfFindings.push({ rule: 'R1f', message: `scope 表里的 \`${v}\` 不是 schema 里的 verb`, id: v })
|
|
362
|
+
|
|
363
|
+
const cli = cliVerbsOf(root)
|
|
364
|
+
const cliVerbs = cli.ok ? { verbs: cli.verbs, subcommands: cli.subcommands } : { verbs: [], subcommands: {} }
|
|
365
|
+
const mcp = mcpToolsOf(root)
|
|
366
|
+
const r1 = checkContractDrift({
|
|
367
|
+
root, contracts, schema, cliVerbs, cliError: cli.ok ? null : cli.error,
|
|
368
|
+
mcpTools: mcp.ok ? mcp.tools : null,
|
|
369
|
+
mcpError: mcp.skipped || mcp.ok ? null : (mcp.error || '未知错误'),
|
|
370
|
+
})
|
|
371
|
+
const r2 = checkRestatement({ root, contracts })
|
|
372
|
+
const drift = [...selfFindings, ...r1.findings]
|
|
373
|
+
return {
|
|
374
|
+
ok: drift.length === 0,
|
|
375
|
+
drift,
|
|
376
|
+
restate: r2.findings,
|
|
377
|
+
stats: { ...r1.stats, mcpTools: mcp.ok ? mcp.tools.length : 0, restateFiles: r2.stats.files, restateTokens: r2.stats.tokens },
|
|
378
|
+
strict, strictContract,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (isDirectRun(import.meta.url)) {
|
|
383
|
+
const res = run()
|
|
384
|
+
if (AS_JSON) console.log(JSON.stringify(res, null, 2))
|
|
385
|
+
else {
|
|
386
|
+
console.log('\n【R1 契约 ↔ 代码(双向;0 容忍)】')
|
|
387
|
+
if (res.fatal) console.log(` ❌ ${res.fatal}`)
|
|
388
|
+
else if (!res.drift.length) console.log(' ✅ 漂移 0 条'
|
|
389
|
+
+ `(verb ${res.stats.verbs} 个 / CLI 实报 ${res.stats.cliVerbs} 个 / MCP tools ${res.stats.mcpTools} 个 | judgment ${res.stats.judgments} 条 / anchor ${res.stats.anchors} 处 | 扫描面 ${res.stats.scanFiles} 文件)`)
|
|
390
|
+
else for (const f of res.drift) console.log(` ❌ [${f.rule}] ${f.message}`)
|
|
391
|
+
console.log('\n【R2 SKILL/提示词复述判断(首日基线;--strict 才红)】')
|
|
392
|
+
if (!res.restate.length) console.log(` ✅ 复述告警 0 条(扫 ${res.stats.restateFiles} 文件 / ${res.stats.restateTokens} token)`)
|
|
393
|
+
else {
|
|
394
|
+
console.log(` ⚠️ 复述告警 ${res.restate.length} 条(扫 ${res.stats.restateFiles} 文件)—— 命中判断常量却无引用点:`)
|
|
395
|
+
for (const f of res.restate.slice(0, 40)) console.log(` · ${f.file}:${f.line} [${f.tokens.join(' + ')}] ${f.text}`)
|
|
396
|
+
if (res.restate.length > 40) console.log(` …(其余 ${res.restate.length - 40} 条省略;--json 看全量)`)
|
|
397
|
+
}
|
|
398
|
+
console.log(`\n 结论:契约漂移 ${res.drift.length} 条 | 复述告警 ${res.restate.length} 条`
|
|
399
|
+
+ `${res.strict || res.strictContract ? '' : '(默认只报告 ⇒ exit 0)'}`)
|
|
400
|
+
}
|
|
401
|
+
const fail = res.fatal ? true : (STRICT ? (res.drift.length + res.restate.length) > 0 : (STRICT_CONTRACT ? res.drift.length > 0 : false))
|
|
402
|
+
process.exit(fail ? 1 : 0)
|
|
403
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-license —— 离线许可状态**只读**自检(2026-09-30)
|
|
4
|
+
*
|
|
5
|
+
* 用途:客户机上不登录问述账号也能回答四个问题 ——
|
|
6
|
+
* ① 这份许可**是不是我们签的**(验签);② 买了哪些能力/业务线;③ 什么时候到期、是否已在宽限期;
|
|
7
|
+
* ④ **这条流程/这个能力现在许不许跑**(`--cap` / `--vertical`,供 flow-trigger 的许可闸门判定)。
|
|
8
|
+
* 不写任何文件、不联网(断网/无账号令牌都能跑)。
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* node scripts/check-license.mjs [--file <path>] [--cap <id>]… [--vertical <id>] [--now <ISO>] [--json]
|
|
12
|
+
* 缺省文件:`$ARGENT_DATA_DIR/license.json`(未设则 `~/.dsh/argent/license.json`)。
|
|
13
|
+
* 密钥来源:`ARGENT_LICENSE_SECRET` 或 `~/.dsh/argent/license.secret`(600)——都没有 ⇒ 明确报 no_secret。
|
|
14
|
+
*
|
|
15
|
+
* 退出码(与 scripts/lease.py 同一套口径,便于流程里 `&&` 串联):
|
|
16
|
+
* 0 允许(验签通过且在有效期内,且 `--cap`/`--vertical` 都命中)
|
|
17
|
+
* 3 宽限(已过期但在宽限期内 ⇒ 放行但必须告警;能力判定此时仍算命中)
|
|
18
|
+
* 1 拒绝(缺失/坏签名/超期/无密钥/**能力或业务线未授权**)
|
|
19
|
+
* 2 用法错误
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ 与旧版的唯一语义差别:给了 `--cap` 时,**能力未授权 ⇒ exit 1**(此前只看许可整体有效性)。
|
|
22
|
+
* 不给 `--cap` 时行为逐字不变。消费方请一律解析 `--json` 的字段,**不要 grep 中文**。
|
|
23
|
+
*/
|
|
24
|
+
import { resolve } from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
import { defaultLicensePath, defaultSecretPath, capabilityAllowed } from '../core/lib/license-file.mjs'
|
|
27
|
+
import { licenseVerdict, stateOf } from '../core/lib/license-verdict.mjs'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 取参数(可重复的参数用 `allOf`)。
|
|
31
|
+
* 未知 `--xxx` 一律忽略(历史行为;`--cap` 未识别时也不能把别人带的参数当值吃掉)。
|
|
32
|
+
*/
|
|
33
|
+
const allOf = (name) => {
|
|
34
|
+
const out = []
|
|
35
|
+
process.argv.forEach((a, i) => { if (a === name) out.push(process.argv[i + 1]) })
|
|
36
|
+
return out.filter((v) => v !== undefined && v !== null && v !== '')
|
|
37
|
+
}
|
|
38
|
+
const argOf = (name, dflt = null) => {
|
|
39
|
+
const i = process.argv.indexOf(name)
|
|
40
|
+
return i > -1 ? process.argv[i + 1] ?? dflt : dflt
|
|
41
|
+
}
|
|
42
|
+
const die = (m) => { console.error(`✗ ${m}`); process.exit(2) }
|
|
43
|
+
const parseNow = (s) => {
|
|
44
|
+
if (s === null || s === undefined || s === '') return null
|
|
45
|
+
const t = Date.parse(String(s))
|
|
46
|
+
return Number.isFinite(t) ? t : NaN
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 三态归一 —— **实现已搬到 `core/lib/license-verdict.mjs`**(唯一判定入口:验签 + 到期 + **设备绑定**)。
|
|
51
|
+
* 本文件只把它**再导出**:既有消费方(`scripts/tests/license-gate.test.mjs` 等)继续按老路径 import,
|
|
52
|
+
* 但**判据只有一份**(§3.5① 纪律 1:判定点唯一,不许在脚本里再写一遍三态)。
|
|
53
|
+
*/
|
|
54
|
+
export { stateOf }
|
|
55
|
+
|
|
56
|
+
function main() {
|
|
57
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
58
|
+
console.log('用法:node scripts/check-license.mjs [--file <path>] [--cap <id>]… [--vertical <id>] [--now <ISO>] [--json]')
|
|
59
|
+
console.log('退出码:0 有效/允许 | 3 宽限 | 1 拒绝(含能力未授权、设备不匹配超宽限) | 2 用法错误')
|
|
60
|
+
process.exit(0)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nowArg = parseNow(argOf('--now'))
|
|
64
|
+
if (Number.isNaN(nowArg)) die(`--now 不是可解析的时间:${argOf('--now')}`)
|
|
65
|
+
|
|
66
|
+
const file = argOf('--file') ? resolve(argOf('--file')) : defaultLicensePath()
|
|
67
|
+
const asJson = process.argv.includes('--json')
|
|
68
|
+
const caps = allOf('--cap')
|
|
69
|
+
const vertical = argOf('--vertical')
|
|
70
|
+
const now = nowArg === null ? Date.now() : nowArg
|
|
71
|
+
|
|
72
|
+
// 唯一判定入口(三态 + 设备绑定;`signatureVersion`/`device.signed` 由验签结果带出,本脚本不重算)
|
|
73
|
+
const r = licenseVerdict({ file, now })
|
|
74
|
+
const granted = r.capabilities.map((c) => [c, capabilityAllowed(r, c, { now })])
|
|
75
|
+
const capGrants = Object.fromEntries(granted)
|
|
76
|
+
const capsOk = caps.every((c) => capabilityAllowed(r, c, { now }))
|
|
77
|
+
const verticals = Array.isArray(r.verticals) ? r.verticals.map(String) : []
|
|
78
|
+
// 业务线口径与能力**不同**(见 license-file.mjs 头注释):verticals 空 = 不限业务线。
|
|
79
|
+
const verticalOk = !vertical || verticals.length === 0 || verticals.includes(String(vertical))
|
|
80
|
+
// 设备这一侧已经进 `r.ok`(grace 放行 / denied 拒绝)⇒ allowed 必须带上它,否则"设备已拒绝"会被能力门洗白
|
|
81
|
+
const allowed = r.ok === true && capsOk && verticalOk
|
|
82
|
+
const gateReason = allowed ? null
|
|
83
|
+
: (r.ok !== true
|
|
84
|
+
// 许可/设备这一侧拒绝:把**自解释的设备 reason**(device_mismatch / rebind_exhausted)透出去。
|
|
85
|
+
// ⚠️ 只在**许可本身验签通过**(`baseOk`)时才让设备原因当顶层 reason —— 否则"这份文件根本没验过签"
|
|
86
|
+
// 会被"设备不匹配"盖住(v2 起尤其要紧:改 `device` 会被判 bad_signature,报设备不匹配 = 误导)。
|
|
87
|
+
? (r.device && r.device.state === 'denied' && r.device.binding === 'mismatch' && r.baseOk === true ? r.device.reason : null)
|
|
88
|
+
: (capsOk ? 'vertical_not_allowed' : 'cap_not_allowed'))
|
|
89
|
+
|
|
90
|
+
const exitCode = allowed ? (r.inGrace ? 3 : 0) : 1
|
|
91
|
+
|
|
92
|
+
// 人类可读的结论(--json 时也打一份,方便日志里直接看到判据)
|
|
93
|
+
const reasonText = {
|
|
94
|
+
ok: '验签通过(有效期内)',
|
|
95
|
+
in_grace: '验签通过,但**已过期**(宽限期内 ⇒ 放行,请尽快续期)',
|
|
96
|
+
missing: `许可文件不存在:${file}`,
|
|
97
|
+
unreadable: '许可文件读不出来',
|
|
98
|
+
malformed: '许可文件不是合法的许可 JSON(字段缺失或时间不可解析)',
|
|
99
|
+
bad_version: '许可版本不支持(本客户端只认 v1/v2)',
|
|
100
|
+
bad_signature: '**验签失败**:文件被改动过,或者不是本机该用的密钥(拒绝)',
|
|
101
|
+
expired: '许可已过期且超出宽限期(拒绝)',
|
|
102
|
+
no_secret: `本机没有签名密钥:设置 ARGENT_LICENSE_SECRET,或放入 ${defaultSecretPath()}(600)`,
|
|
103
|
+
}[r.reason] || r.reason
|
|
104
|
+
// 设备这一项到底有没有进签名:文案按**签名版本**动态化(v2 不再说"未进签名")
|
|
105
|
+
const deviceSignedText = (() => {
|
|
106
|
+
const dev = r.device || {}
|
|
107
|
+
if (r.signatureVersion === 2) {
|
|
108
|
+
return dev.present
|
|
109
|
+
? '签名版本 **v2**:`device` **已进签名**(载荷 10 项,含 device{code,algo,issuedAt})'
|
|
110
|
+
+ ' ⇒ 改它/摘它/降 v 重拼都会验签失败;⚠️ 签名仍是 HMAC(对称密钥)⇒ 不防"持有密钥的人重签"'
|
|
111
|
+
: '签名版本 **v2**:本许可**未声明**设备(unbound)—— **缺位也被签名覆盖**(事后加 `device` 会验签失败)⇒ 不做比对、放行'
|
|
112
|
+
}
|
|
113
|
+
return '`device` **未进签名**(v1 只签 9 项)⇒ 它是“服务端声明 + 客户端比对”,不是防伪'
|
|
114
|
+
})()
|
|
115
|
+
const gateText = {
|
|
116
|
+
cap_not_allowed: `许可里没有这个能力(要 ${caps.join(',')},许可只有 ${r.capabilities.join(',') || '(空)'})⇒ 拒绝`,
|
|
117
|
+
vertical_not_allowed: `许可不含业务线 ${vertical}(许可业务线:${verticals.join(',') || '(空=不限)'})⇒ 拒绝`,
|
|
118
|
+
// 设备不匹配的两种拒绝形态:把 `device.note`(含"下一步做什么")原样透出,不另写一套文案
|
|
119
|
+
device_mismatch: r.device ? r.device.note : null,
|
|
120
|
+
rebind_exhausted: r.device ? r.device.note : null,
|
|
121
|
+
}[gateReason] || null
|
|
122
|
+
|
|
123
|
+
const out = {
|
|
124
|
+
ok: allowed,
|
|
125
|
+
state: r.state,
|
|
126
|
+
reason: r.reason,
|
|
127
|
+
conclusion: [reasonText, gateText].filter(Boolean).join(' | '),
|
|
128
|
+
path: file,
|
|
129
|
+
mode: r.mode === null || r.mode === undefined ? null : `0${r.mode.toString(8)}`,
|
|
130
|
+
userId: r.userId,
|
|
131
|
+
capabilities: r.capabilities,
|
|
132
|
+
verticals: r.verticals,
|
|
133
|
+
seats: r.seats,
|
|
134
|
+
issuedAt: r.issuedAt,
|
|
135
|
+
expiresAt: r.expiresAt,
|
|
136
|
+
graceDays: r.graceDays,
|
|
137
|
+
graceUntil: r.graceUntil,
|
|
138
|
+
inGrace: r.inGrace,
|
|
139
|
+
// 宽限来自哪一侧(`expiry` 许可到期 | `device` 设备不匹配 | `expiry+device` 两侧都命中)——
|
|
140
|
+
// 空/异常结果必须能一眼分出"真没有"与"被别的判据盖住"(A2 的回显纪律)
|
|
141
|
+
graceSource: r.graceSource,
|
|
142
|
+
// ── 签名版本(1|2|null)+ 设备是否进签名(v1 ⇒ signed:false;v2 ⇒ signed:true)──
|
|
143
|
+
signatureVersion: r.signatureVersion === 2 ? 2 : (r.signatureVersion === 1 ? 1 : null),
|
|
144
|
+
deviceSigned: r.deviceSigned === true,
|
|
145
|
+
signature: {
|
|
146
|
+
version: r.signatureVersion === 2 ? 2 : (r.signatureVersion === 1 ? 1 : null),
|
|
147
|
+
// 验签本身的结论(ok/in_grace/bad_signature/expired/malformed/bad_version/no_secret/…)
|
|
148
|
+
reason: r.reason,
|
|
149
|
+
// `device` 这一项是否被签名载荷覆盖(**只有 v2 ∧ 验签通过**才为 true,见 license-file.mjs 头注释)
|
|
150
|
+
deviceSigned: r.deviceSigned === true,
|
|
151
|
+
algorithm: 'HMAC-SHA256',
|
|
152
|
+
},
|
|
153
|
+
// ── 设备绑定(§3.6;`device.signed:true` = v2 已进签名;`false` = v1 未进签名:服务端声明 + 客户端比对)──
|
|
154
|
+
device: r.device,
|
|
155
|
+
// ── 请求侧(闸门判定)──────────────────────────────────────────
|
|
156
|
+
requestedCaps: caps,
|
|
157
|
+
requestedVertical: vertical || null,
|
|
158
|
+
// 有效 = 验签过 ∧ 设备这一侧放行 ∧ 请求的能力都授权 ∧ 业务线匹配(没请求就只看许可本身)
|
|
159
|
+
allowed,
|
|
160
|
+
gateReason,
|
|
161
|
+
// 能力门禁的可用形态:`--json` 的消费方(脚本/宿主)可以直接照此判
|
|
162
|
+
capabilityGate: capGrants,
|
|
163
|
+
warnings: r.warnings,
|
|
164
|
+
}
|
|
165
|
+
if (asJson) {
|
|
166
|
+
console.log(JSON.stringify(out, null, 2))
|
|
167
|
+
} else {
|
|
168
|
+
const mark = allowed ? (r.inGrace ? '⚠️' : '✅') : '❌'
|
|
169
|
+
const graceFrom = { expiry: '到期', device: '设备不匹配', 'expiry+device': '到期 + 设备不匹配' }[r.graceSource]
|
|
170
|
+
console.log(`${mark} 离线许可:${reasonText}${graceFrom ? `(宽限来自:${graceFrom})` : ''}`)
|
|
171
|
+
console.log(` 文件:${file}${out.mode ? `(权限 ${out.mode}${out.mode === '0600' ? '' : ' ⚠️ 建议 600'})` : ''}`)
|
|
172
|
+
console.log(` 签名版本:${out.signatureVersion === null ? '判不出(缺文件/缺密钥/坏文件 ⇒ 见上面的 reason)' : `v${out.signatureVersion}`}`
|
|
173
|
+
+ ` | device 进签名:${out.signature.deviceSigned ? '是' : '否'}`)
|
|
174
|
+
if (r.expiresAt) {
|
|
175
|
+
console.log(` 账号:${r.userId || '(未写)'} | 席位:${r.seats}`)
|
|
176
|
+
console.log(` 能力:${r.capabilities.join(',') || '(未列能力 ⇒ 客户端拒一切能力)'}`)
|
|
177
|
+
console.log(` 业务线:${r.verticals.join(',') || '(空 = 不限)'}`)
|
|
178
|
+
console.log(` 签发:${r.issuedAt} | 到期:${r.expiresAt} | 宽限 ${r.graceDays} 天(至 ${r.graceUntil})`)
|
|
179
|
+
}
|
|
180
|
+
if (r.device) {
|
|
181
|
+
console.log(` 设备绑定:${r.device.binding}(${r.device.reason})`
|
|
182
|
+
+ `${r.device.present ? ` | 许可设备码 ${String(r.device.code).slice(0, 12)}…` : ''}`
|
|
183
|
+
+ `${r.device.machineCode ? ` | 本机设备码 ${r.device.machineCode.slice(0, 12)}…` : ''}`
|
|
184
|
+
+ ` | 自助重绑还剩 ${r.device.rebindsLeft} 次`)
|
|
185
|
+
console.log(` ${deviceSignedText}`)
|
|
186
|
+
if (r.device.note) console.log(` · ${r.device.note}`)
|
|
187
|
+
}
|
|
188
|
+
if (caps.length || vertical) {
|
|
189
|
+
console.log(` 本次询问:能力=${caps.join(',') || '(未指定)'} | 业务线=${vertical || '(未指定)'}`)
|
|
190
|
+
console.log(` 判定:${allowed ? '允许' : '拒绝'}${gateText ? `(${gateText})` : ''}`)
|
|
191
|
+
}
|
|
192
|
+
for (const w of r.warnings) console.log(` ⚠️ ${w}`)
|
|
193
|
+
}
|
|
194
|
+
process.exit(exitCode)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 只在**直接运行**时执行:被 import(回归测试要用 stateOf)时不得产生副作用
|
|
198
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) main()
|
|
199
|
+
|