@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,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-sample-redaction.mjs —— 官网样例**脱敏自证门禁**(只读)
|
|
4
|
+
*
|
|
5
|
+
* 为什么有它:`docs/samples/ml-daily-report-sample.{json,md}` 是给官网展示页用的
|
|
6
|
+
* **真实日报样例**。一旦有人"顺手更新样例"时把真实店铺名 / 商品标题 / SKU / 单号 /
|
|
7
|
+
* 原始金额粘回来,肉眼极难发现 —— 脱敏这种事**必须机器判**,不能靠 review 记得。
|
|
8
|
+
*
|
|
9
|
+
* ## 两件事,一次跑完
|
|
10
|
+
* | 检查 | 判据 |
|
|
11
|
+
* |---|---|
|
|
12
|
+
* | **A 禁用串扫描** | 两份交付物里出现**任何**能定位到具体客户 / 店铺 / 商品 / 订单 / 账号的串,或**原始金额·销量数字** ⇒ 命中(详见 {@link BANNED} 与 {@link HASHED_TOKENS}) |
|
|
13
|
+
* | **B 键名一致性** | 样例 JSON 的 `schema` / `note` / 每个 `section.id` / 每个 `metrics[].name`,必须与**真源指标清单**(`docs/` 下的「数据指标清单.json」)**逐条对齐**(防样例与指标清单各写一份而悄悄漂移) |
|
|
14
|
+
*
|
|
15
|
+
* ## 禁用串清单的分档理由
|
|
16
|
+
* - **形状规则**({@link BANNED},9 条明文):商品 ID 形态(`ML[A-Z]+<6+位数字>`)/ SKU 形态
|
|
17
|
+
* (`XX-XX-<数字>`)/ 账号 ID 形态(10 位以上连续数字)/ 飞书与 Base ID 形态 /
|
|
18
|
+
* 带标签的单号 / 邮箱 / 禁用字样。这类只描述"长相",**不含任何客户原文**。
|
|
19
|
+
* - **单向摘要**({@link HASHED_TOKENS},43 条 `sha256` 前 16 位):取材日报里出现过的
|
|
20
|
+
* **原始金额与销量/件数**、店铺账号 ID、SKU 品牌缩写、客户·群·人名。
|
|
21
|
+
* ⛔ **刻意不写明文** —— 禁用串清单若把原始值原样列出来,等于**把原始客户数据又落进仓库一次**,
|
|
22
|
+
* 正是本门禁要防的事;摘要单向、不可还原,比对时明文只出现在交付物那一侧。
|
|
23
|
+
* 维护法见 {@link HASHED_TOKENS} 的注释与 `--hash`。
|
|
24
|
+
* - **短数字刻意不入清单**:四位以下的量(客单价、订单数这类)满页都是,禁了就是恒红陷阱
|
|
25
|
+
* (假阳性比漏报更伤:会骗人去"修"一个没坏的东西)。
|
|
26
|
+
* - **无量纲量(率 / 占比 / 可售天数 / 最久等待时长)刻意不禁**:它们对缩放不变,
|
|
27
|
+
* 且单独任何一个都不构成定位信息(`0.41%` 纠纷率 + `5_green` 等级定位不到任何一家店)。
|
|
28
|
+
* 本清单只钉"可搜索原文"与"能反推规模"的两类。
|
|
29
|
+
*
|
|
30
|
+
* ## 用法
|
|
31
|
+
* node scripts/check-sample-redaction.mjs # 只报告,**恒 exit 0**
|
|
32
|
+
* node scripts/check-sample-redaction.mjs --strict # 有命中才 exit 1(CI 判据用)
|
|
33
|
+
* node scripts/check-sample-redaction.mjs --json # 机器可读
|
|
34
|
+
* node scripts/check-sample-redaction.mjs --list-rules
|
|
35
|
+
* node scripts/check-sample-redaction.mjs --root <dir> # 换根目录(自测夹具用;仍然只读)
|
|
36
|
+
* node scripts/check-sample-redaction.mjs --hash <原文> # 维护用:打印摘要(明文不入库)
|
|
37
|
+
*
|
|
38
|
+
* ## 纪律
|
|
39
|
+
* 只读:不改文件、不写台账/数据目录、不读凭据、不联网。判据面只有 `docs/` 下的三个文件
|
|
40
|
+
* (两份样例 + 一份真源指标清单)。
|
|
41
|
+
*/
|
|
42
|
+
import { createHash } from 'node:crypto'
|
|
43
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
44
|
+
import { dirname, join, resolve } from 'node:path'
|
|
45
|
+
import { fileURLToPath } from 'node:url'
|
|
46
|
+
import { isDirectRun } from './lib/is-direct-run.mjs'
|
|
47
|
+
|
|
48
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
49
|
+
const DEFAULT_ROOT = resolve(here, '..')
|
|
50
|
+
|
|
51
|
+
const SAMPLE_JSON = 'docs/samples/ml-daily-report-sample.json'
|
|
52
|
+
const SAMPLE_MD = 'docs/samples/ml-daily-report-sample.md'
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 真源指标清单的**发现式**定位 —— L1 引擎里刻意**不写业务线名**
|
|
56
|
+
* (`scripts/` 不得出现业务词,见 `scripts/vertical-lint.mjs` 的 engine-words 规则与
|
|
57
|
+
* `verticals/_engine-words.json`)。判据 = 文件名以「数据指标清单.json」结尾
|
|
58
|
+
* **且** `schema` 命中 {@link INVENTORY_SCHEMA}(不靠路径硬编码,清单改名也不会静默放过)。
|
|
59
|
+
*/
|
|
60
|
+
const INVENTORY_SCHEMA = 'argent-ml-daily-metrics/1'
|
|
61
|
+
const INVENTORY_SUFFIX = '数据指标清单.json'
|
|
62
|
+
|
|
63
|
+
/** 在 `<root>/docs/` 下发现真源指标清单;返回相对路径或 `null`。只读。 */
|
|
64
|
+
export function findInventoryFile(root = DEFAULT_ROOT) {
|
|
65
|
+
const dir = join(root, 'docs')
|
|
66
|
+
if (!existsSync(dir)) return null
|
|
67
|
+
for (const name of readdirSync(dir).sort()) {
|
|
68
|
+
if (!name.endsWith(INVENTORY_SUFFIX)) continue
|
|
69
|
+
try {
|
|
70
|
+
if (JSON.parse(readFileSync(join(dir, name), 'utf8')).schema === INVENTORY_SCHEMA) return `docs/${name}`
|
|
71
|
+
} catch { /* 同名但解析失败 ⇒ 不是它,继续找 */ }
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 样例 JSON 的固定头(与官网消费方约定的契约)。 */
|
|
77
|
+
export const SAMPLE_SCHEMA = 'argent-ml-daily-sample/1'
|
|
78
|
+
export const SAMPLE_NOTE = '示例数据(已脱敏,非真实客户数据)'
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* **形状**规则(不含任何客户原文)—— 描述"什么长相的串不该出现",
|
|
82
|
+
* 命中时报告里点名的是**规则**(如 `sku-shape`),不是某个具体客户值。
|
|
83
|
+
*/
|
|
84
|
+
export const BANNED = [
|
|
85
|
+
{ id: 'account-id', why: '店铺 / 卖家账号 ID 形态(10 位以上连续数字)', re: /\b\d{10,}\b/g },
|
|
86
|
+
{ id: 'item-id', why: '电商平台商品 ID 形态(ML + 字母站点码 + 6 位以上数字)', re: /\bML[A-Z]\d{6,}\b/g },
|
|
87
|
+
{ id: 'sku-shape', why: 'SKU 形态(<前缀>-MX-<数字>,真实 SKU 即此形)', re: /\b[A-Za-z]{2,4}-MX-[A-Za-z0-9-]*\d[A-Za-z0-9-]*/g },
|
|
88
|
+
{ id: 'sku-generic', why: '通用 SKU 形态(<2-4 字母>-<2 大写字母>-<数字>)', re: /\b[A-Za-z]{2,4}-[A-Z]{2}-\d[A-Za-z0-9-]*/g },
|
|
89
|
+
{ id: 'feishu-id', why: '飞书 / Base / 表 ID 形态(chat_id、open_id、base token、table id)', re: /\b(?:oc_|ou_|om_|cli_|tbl|bas|fld)[A-Za-z0-9_]{8,}/g },
|
|
90
|
+
{ id: 'record-id', why: 'Base 记录号形态', re: /\brec[A-Za-z0-9]{8,}\b/g },
|
|
91
|
+
{ id: 'labelled-number', why: '带标签的原始单号 / 卖家 ID(订单号 / 货件号 / 包裹号 / 卖家 ID + 4 位以上数字)', re: /(?:订单号|货件号|包裹号|卖家\s*ID)\s*[::]?\s*\d{4,}/g },
|
|
92
|
+
{ id: 'email-shape', why: '邮箱形态(客户 / 联系人标识)', re: /@[A-Za-z0-9.-]+\.(?:com|cn|net)/g },
|
|
93
|
+
{ id: 'banned-phrase', why: '交付物里不得出现"某客户真实数据"这类字样', re: /某客户真实数据/g },
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* **具体客户原文的单向摘要**清单(`sha256(token)` 的前 16 位十六进制)。
|
|
98
|
+
*
|
|
99
|
+
* ### 为什么是摘要而不是明文
|
|
100
|
+
* 禁用串清单天然要写"禁什么",但**把取材日报的原始金额 / 销量、店铺账号 ID、品牌缩写、
|
|
101
|
+
* 客户与群名原样写进这个脚本,等于把原始客户数据又落进仓库一次** —— 那正是本门禁要防的事。
|
|
102
|
+
* 摘要单向、不可还原,比对时**明文只出现在交付物那一侧**(命中报告里点名的是交付物里的
|
|
103
|
+
* 那一段,本来就已经在交付物里,不构成新增泄露)。
|
|
104
|
+
*
|
|
105
|
+
* ### 覆盖范围(43 条)
|
|
106
|
+
* · 取材日报的原始**金额**(GMV / 佣金 / 优惠券 / 商品 7 日销售额);
|
|
107
|
+
* · 原始**销量 / 件数**(订单数、件数、累计已售、访问量、访客 Top5、SKU 数);
|
|
108
|
+
* · 店铺 / 卖家**账号 ID**(同时被 `account-id` 形状规则兜住,摘要提供"点名到具体值"的能力);
|
|
109
|
+
* · 真实 SKU 的**品牌缩写**前缀;
|
|
110
|
+
* · **客户 / 群 / 人**名。
|
|
111
|
+
*
|
|
112
|
+
* ### 怎么补新条目(维护动作)
|
|
113
|
+
* 拿到新一批取材数据后,对每个原始值跑一次
|
|
114
|
+
* `node scripts/check-sample-redaction.mjs --hash <原文>`,把打印出的摘要追加到下面即可 ——
|
|
115
|
+
* **明文只在你的终端里出现一次,不进仓库、不进提交、不进 CI 日志**。
|
|
116
|
+
*/
|
|
117
|
+
export const HASHED_TOKENS = new Set([
|
|
118
|
+
'5d207cf244a249e8', '7cf1c96f66f8e87b', 'f8de3df2c078baa7', '89d295d75a9717e8',
|
|
119
|
+
'cb0b20f98ee49533', 'f4dd96228b9a0d6e', 'c3c8204aec16162a', '0304631267d2f0e1',
|
|
120
|
+
'a0337a650ec85c55', '4f9f73b34c5b8987', '876c9b16254e157d', 'a037a7dfaf26abe1',
|
|
121
|
+
'55fdec963805de59', '890fd025edbcc524', '47b25095a0fe135a', '0944d4d0aabb9305',
|
|
122
|
+
'9c978dcb04837772', 'e36deec09aa73922', 'e3be6803d0ccd399', 'b97d4904938f12a0',
|
|
123
|
+
'0dc89b2fb404b4d7', '3ee78724cb832ac7', '0dc5713ce820f082', '262b06d105e1c865',
|
|
124
|
+
'3f9eb849d10083d5', 'cb209f169ae04866', 'dc50a94325365105', '614f5408eab24ce6',
|
|
125
|
+
'43908979b8b2d28d', 'db6479551754db9b', 'e06f88267f246de6', 'b700bd6ebf3dadab',
|
|
126
|
+
'6317984746f70826', '5a4a0c923c9a9f9e', '0b689c5667de7f17', '2bb1c90302bea604',
|
|
127
|
+
'69898c7bb333c6ad', '01ba9d7ab5112ec9', '91660cd41bd4fe15', '9889a3fb03c5db96',
|
|
128
|
+
'1882b4902a76e8c8', 'a46569295c2eb8af', 'f413de3a975813cb',
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
/** `sha256(token)` 的前 16 位十六进制 —— 与 {@link HASHED_TOKENS} 同口径。 */
|
|
132
|
+
export const tokenDigest = (token) => createHash('sha256').update(String(token), 'utf8').digest('hex').slice(0, 16)
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 从交付物里抽出候选 token(**只读交付物那一侧**):
|
|
136
|
+
* · 数字 —— 去掉千分位后 3 位以上(故 `1234` 与 `1,234` 会归一到同一个摘要);
|
|
137
|
+
* · ASCII 词 —— 转小写(故 `HW` / `hw` 同一个摘要);
|
|
138
|
+
* · CJK —— 连续汉字段的全部 2–4 元组(人名 / 群名不靠分词也能被摘要命中)。
|
|
139
|
+
*/
|
|
140
|
+
export function candidateTokens(text) {
|
|
141
|
+
const out = new Set()
|
|
142
|
+
for (const m of String(text).matchAll(/\d[\d,]*/g)) {
|
|
143
|
+
const t = m[0].replace(/,/g, '')
|
|
144
|
+
if (/^\d{3,}$/.test(t)) out.add(t)
|
|
145
|
+
}
|
|
146
|
+
for (const m of String(text).matchAll(/[A-Za-z][A-Za-z0-9_-]*/g)) out.add(m[0].toLowerCase())
|
|
147
|
+
for (const m of String(text).matchAll(/[\u4e00-\u9fff]{2,}/g)) {
|
|
148
|
+
const run = m[0]
|
|
149
|
+
for (let n = 2; n <= 4; n++) for (let i = 0; i + n <= run.length; i++) out.add(run.slice(i, i + n))
|
|
150
|
+
}
|
|
151
|
+
return out
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 摘要比对:命中 ⇒ 报告里点名**交付物本身**的原文片段(不引入任何新明文)。 */
|
|
155
|
+
export function scanHashedTokens(file, text, digests = HASHED_TOKENS) {
|
|
156
|
+
const out = []
|
|
157
|
+
const lines = String(text).split('\n')
|
|
158
|
+
for (const tok of candidateTokens(text)) {
|
|
159
|
+
const h = tokenDigest(tok)
|
|
160
|
+
if (!digests.has(h)) continue
|
|
161
|
+
const idx = lines.findIndex((l) => l.includes(tok))
|
|
162
|
+
out.push({
|
|
163
|
+
file, line: idx >= 0 ? idx + 1 : 0, id: `banned-token:${h}`,
|
|
164
|
+
why: '命中取材日报的原始金额 / 销量 / 客户标识(明文不入库,按单向摘要比对)',
|
|
165
|
+
text: idx >= 0 ? lines[idx].trim().slice(0, 120) : tok,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 逐行扫一个禁用串清单。返回命中数组(含 `file` / `line` / `id` / `why` / `text`)。 */
|
|
172
|
+
export function scanText(file, text, rules = BANNED) {
|
|
173
|
+
const out = []
|
|
174
|
+
const lines = text.split('\n')
|
|
175
|
+
for (const rule of rules) {
|
|
176
|
+
for (let i = 0; i < lines.length; i++) {
|
|
177
|
+
rule.re.lastIndex = 0
|
|
178
|
+
if (rule.re.test(lines[i])) {
|
|
179
|
+
out.push({ file, line: i + 1, id: rule.id, why: rule.why, text: lines[i].trim().slice(0, 120) })
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 键名一致性:样例 JSON ↔ 指标清单。
|
|
188
|
+
* 判据:`schema` / `note` 逐字一致;每个 `section.id` 命中清单的 `sections[].id`;
|
|
189
|
+
* 每个 `metrics[].name` 命中**同一分区**的 `metrics[].name`。
|
|
190
|
+
*/
|
|
191
|
+
export function checkKeyAlignment(sample, inventory) {
|
|
192
|
+
const problems = []
|
|
193
|
+
if (sample.schema !== SAMPLE_SCHEMA) problems.push({ id: 'schema', why: `schema 必须是 ${SAMPLE_SCHEMA}`, got: String(sample.schema) })
|
|
194
|
+
if (sample.note !== SAMPLE_NOTE) problems.push({ id: 'note', why: `note 必须是「${SAMPLE_NOTE}」`, got: String(sample.note) })
|
|
195
|
+
if (!Array.isArray(sample.sections) || !sample.sections.length) {
|
|
196
|
+
problems.push({ id: 'sections', why: 'sections 必须是非空数组', got: String(sample.sections && sample.sections.length) })
|
|
197
|
+
return problems
|
|
198
|
+
}
|
|
199
|
+
const inv = new Map((inventory.sections || []).map((s) => [s.id, new Set((s.metrics || []).map((m) => m.name))]))
|
|
200
|
+
const seen = new Set()
|
|
201
|
+
for (const sec of sample.sections) {
|
|
202
|
+
if (!sec || typeof sec.id !== 'string') { problems.push({ id: 'section.id', why: 'section 缺 id', got: JSON.stringify(sec).slice(0, 80) }); continue }
|
|
203
|
+
if (seen.has(sec.id)) problems.push({ id: `dup-section:${sec.id}`, why: '分区 id 重复', got: sec.id })
|
|
204
|
+
seen.add(sec.id)
|
|
205
|
+
if (!inv.has(sec.id)) { problems.push({ id: `unknown-section:${sec.id}`, why: '分区 id 不在指标清单里(键名漂移)', got: sec.id }); continue }
|
|
206
|
+
if (typeof sec.title !== 'string' || !sec.title) problems.push({ id: `no-title:${sec.id}`, why: '分区缺 title', got: sec.id })
|
|
207
|
+
if (typeof sec.summary !== 'string' || !sec.summary) problems.push({ id: `no-summary:${sec.id}`, why: '分区缺 summary', got: sec.id })
|
|
208
|
+
if (!Array.isArray(sec.metrics) || !sec.metrics.length) { problems.push({ id: `no-metrics:${sec.id}`, why: '分区缺 metrics', got: sec.id }); continue }
|
|
209
|
+
const names = inv.get(sec.id)
|
|
210
|
+
for (const m of sec.metrics) {
|
|
211
|
+
if (!m || typeof m.name !== 'string') { problems.push({ id: `metric.name:${sec.id}`, why: '指标缺 name', got: JSON.stringify(m).slice(0, 80) }); continue }
|
|
212
|
+
if (typeof m.unit !== 'string') problems.push({ id: `metric.unit:${sec.id}`, why: '指标缺 unit(复合值写空串)', got: m.name })
|
|
213
|
+
if (m.value === undefined || m.value === null || m.value === '') problems.push({ id: `metric.value:${sec.id}`, why: '指标缺 value', got: m.name })
|
|
214
|
+
if (!names.has(m.name)) problems.push({ id: `metric-name-drift:${sec.id}`, why: '指标名不在指标清单的同名分区里(键名漂移)', got: m.name })
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return problems
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 主检查:返回 `{ hits, problems, stats }`(纯函数,不打印、不退出)。 */
|
|
221
|
+
export function runCheck(root = DEFAULT_ROOT) {
|
|
222
|
+
const hits = []
|
|
223
|
+
const problems = []
|
|
224
|
+
const read = (rel) => (existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : null)
|
|
225
|
+
|
|
226
|
+
const jsonText = read(SAMPLE_JSON)
|
|
227
|
+
const mdText = read(SAMPLE_MD)
|
|
228
|
+
const invRel = findInventoryFile(root)
|
|
229
|
+
const invText = invRel ? read(invRel) : null
|
|
230
|
+
|
|
231
|
+
for (const [rel, text] of [[SAMPLE_JSON, jsonText], [SAMPLE_MD, mdText]]) {
|
|
232
|
+
if (text == null) problems.push({ id: 'missing-file', why: '交付物不存在', got: rel })
|
|
233
|
+
else {
|
|
234
|
+
hits.push(...scanText(rel, text))
|
|
235
|
+
hits.push(...scanHashedTokens(rel, text))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let stats = { sections: 0, metrics: 0 }
|
|
240
|
+
if (jsonText != null) {
|
|
241
|
+
let sample = null
|
|
242
|
+
try { sample = JSON.parse(jsonText) } catch (e) { problems.push({ id: 'json-parse', why: '样例 JSON 解析失败', got: String(e.message) }) }
|
|
243
|
+
if (sample) {
|
|
244
|
+
if (invText == null) problems.push({ id: 'missing-inventory', why: '真源指标清单未发现(文件名以「数据指标清单.json」结尾且 schema 命中),无法校验键名一致性', got: `docs/**/${INVENTORY_SUFFIX}` })
|
|
245
|
+
else {
|
|
246
|
+
let inv = null
|
|
247
|
+
try { inv = JSON.parse(invText) } catch (e) { problems.push({ id: 'inventory-parse', why: '指标清单 JSON 解析失败', got: String(e.message) }) }
|
|
248
|
+
if (inv) problems.push(...checkKeyAlignment(sample, inv))
|
|
249
|
+
}
|
|
250
|
+
stats = {
|
|
251
|
+
sections: (sample.sections || []).length,
|
|
252
|
+
metrics: (sample.sections || []).reduce((a, s) => a + ((s.metrics || []).length), 0),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// MD 必须能让官网页照抄:每个分区标题都要在 MD 里出现
|
|
258
|
+
if (jsonText && mdText) {
|
|
259
|
+
try {
|
|
260
|
+
for (const sec of JSON.parse(jsonText).sections || []) {
|
|
261
|
+
if (typeof sec.title === 'string' && sec.title && !mdText.includes(sec.title)) {
|
|
262
|
+
problems.push({ id: `md-missing-section:${sec.id}`, why: '人读版缺少该分区标题', got: sec.title })
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch { /* 解析失败已在上面报过 */ }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
hits.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1))
|
|
269
|
+
return { hits, problems, stats }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── CLI ────────────────────────────────────────────────────────────────────
|
|
273
|
+
function main(argv) {
|
|
274
|
+
// 维护工具:把一个原文转成摘要(明文只在终端出现一次,不进仓库 / 提交 / CI 日志)
|
|
275
|
+
const hashIdx = argv.indexOf('--hash')
|
|
276
|
+
if (hashIdx >= 0) {
|
|
277
|
+
const tok = argv[hashIdx + 1]
|
|
278
|
+
if (!tok) { console.error('用法:node scripts/check-sample-redaction.mjs --hash <原文>'); return 2 }
|
|
279
|
+
console.log(tokenDigest(tok))
|
|
280
|
+
return 0
|
|
281
|
+
}
|
|
282
|
+
if (argv.includes('--list-rules')) {
|
|
283
|
+
console.log(`禁用串清单 = ${BANNED.length} 条**形状**规则 + ${HASHED_TOKENS.size} 条**单向摘要**(具体客户原文一律不入库)`)
|
|
284
|
+
for (const r of BANNED) console.log(` · ${r.id.padEnd(20)} ${r.why}`)
|
|
285
|
+
console.log(` · ${'banned-token:*'.padEnd(20)} 取材日报的原始金额 / 销量 / 客户标识(按 sha256 前 16 位比对)`)
|
|
286
|
+
return 0
|
|
287
|
+
}
|
|
288
|
+
const rootIdx = argv.indexOf('--root')
|
|
289
|
+
const root = rootIdx >= 0 ? resolve(argv[rootIdx + 1]) : DEFAULT_ROOT
|
|
290
|
+
const strict = argv.includes('--strict')
|
|
291
|
+
const asJson = argv.includes('--json')
|
|
292
|
+
|
|
293
|
+
const { hits, problems, stats } = runCheck(root)
|
|
294
|
+
|
|
295
|
+
if (asJson) {
|
|
296
|
+
console.log(JSON.stringify({ ok: hits.length === 0 && problems.length === 0, root, stats, hits, problems }, null, 2))
|
|
297
|
+
return strict && (hits.length || problems.length) ? 1 : 0
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log(`[sample-redaction] 根目录 ${root}`)
|
|
301
|
+
console.log(`[sample-redaction] 样例规模:${stats.sections} 个分区 / ${stats.metrics} 条指标`)
|
|
302
|
+
console.log(`[sample-redaction] 禁用串清单:${BANNED.length} 条形状规则 + ${HASHED_TOKENS.size} 条单向摘要(${SAMPLE_JSON} + ${SAMPLE_MD})`)
|
|
303
|
+
if (hits.length) {
|
|
304
|
+
console.log(`\n❌ 禁用串命中 ${hits.length} 处:`)
|
|
305
|
+
for (const h of hits) console.log(` ${h.file}:${h.line} [${h.id}] ${h.why}\n ${h.text}`)
|
|
306
|
+
} else {
|
|
307
|
+
console.log('✅ 禁用串命中 0 处')
|
|
308
|
+
}
|
|
309
|
+
if (problems.length) {
|
|
310
|
+
console.log(`\n❌ 结构与键名问题 ${problems.length} 处:`)
|
|
311
|
+
for (const p of problems) console.log(` [${p.id}] ${p.why} —— ${p.got}`)
|
|
312
|
+
} else {
|
|
313
|
+
console.log('✅ 结构与键名一致(schema / note / 分区 id / 指标名与指标清单逐条对齐)')
|
|
314
|
+
}
|
|
315
|
+
const bad = hits.length + problems.length
|
|
316
|
+
console.log(`\n结论:${bad === 0 ? '✅ 通过' : `❌ 不通过(${bad} 处)`}${strict ? '' : ' (默认只报告;加 --strict 才非零退出)'}`)
|
|
317
|
+
return strict && bad ? 1 : 0
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)))
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-skill-classification.mjs —— **技能归类**门禁(P3,§6.1 D5;2026-10-03)
|
|
4
|
+
*
|
|
5
|
+
* ## 它防的是什么(R3:SKILL 外发泄露内部角色)
|
|
6
|
+
* `skills/` 是**混装**目录:客户可用的流程技能与我方内部角色技能住在一起,而升级器是**全量同步**
|
|
7
|
+
* (`scripts/argent-upgrade.mjs` 对每个子目录 `rmSync` + `cpSync`,**无角色过滤**)⇒
|
|
8
|
+
* **往 `skills/` 里丢一个新目录,等于默认把它发给每个客户**。这个门禁把"默认放行"改成
|
|
9
|
+
* "**必须做一次决定**":新技能要么在业务线声明里(外发),要么在内部清单里(不外发),
|
|
10
|
+
* **两边都没有 ⇒ 红**(与 kernel-purity 的"未归类即红"、D5 的判据同精神)。
|
|
11
|
+
*
|
|
12
|
+
* ## 判据(全部来自仓库里的**声明文件**,门禁不另写第二份名单)
|
|
13
|
+
* | # | 判据 | 违反 |
|
|
14
|
+
* |---|---|---|
|
|
15
|
+
* | G1 | `skills/internal.json` 必须存在且可解析(内部半边**必须是清单文件**,不许把规则写死在安装脚本里) | ❌ |
|
|
16
|
+
* | G2 | `skills/` 下的每个技能目录(含 `SKILL.md`)∈ 外发集 ∪ 内部集 | ❌ `未归类` |
|
|
17
|
+
* | G3 | **外发集 = 所有 `verticals/<id>/manifest.json#skills` 的并集**(唯一真源;照抄 manifest 即漂移,故不另立名单) | — |
|
|
18
|
+
* | G4 | 外发集里的每个技能目录必须存在(声明了却没有 ⇒ 装机时静默少一个) | ❌ |
|
|
19
|
+
* | G5 | 内部集里的每个技能目录必须存在(过期条目 = 名单在腐烂) | ❌ |
|
|
20
|
+
* | G6 | 内部集每条必须带**非空理由**(没有理由的"内部"半年后没人敢删) | ❌ |
|
|
21
|
+
* | G7 | 外发集 ∩ 内部集 = ∅(同一个技能不能既外发又不外发) | ❌ |
|
|
22
|
+
* | G8 | `skills/` 下的非技能目录(没有 `SKILL.md`)只**提示**,不判红(可能是 references/模板/在建件) | ⚠️ |
|
|
23
|
+
*
|
|
24
|
+
* ## 用法与退出码
|
|
25
|
+
* ```
|
|
26
|
+
* node scripts/check-skill-classification.mjs # 只报告
|
|
27
|
+
* node scripts/check-skill-classification.mjs --json
|
|
28
|
+
* node scripts/check-skill-classification.mjs --root <dir> # 换根(夹具自证用)
|
|
29
|
+
* node scripts/check-skill-classification.mjs --strict-warn # 把"未带 SKILL.md 的目录"也判红
|
|
30
|
+
* ```
|
|
31
|
+
* · exit `0` 全部合规;`1` 有 ❌;`2` 用法错误(未知参数 / 根不存在)。
|
|
32
|
+
*
|
|
33
|
+
* ## 纪律
|
|
34
|
+
* · **只读**:不写任何文件、不动 `~/.dsh`、不联网;
|
|
35
|
+
* · `classify()` 是**唯一实现**:`scripts/skill-install.mjs` 复用同一个函数(安装器不得自己再判一遍
|
|
36
|
+
* —— 那正是"判据两份"的老事故形态)。
|
|
37
|
+
*/
|
|
38
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
39
|
+
import { dirname, join, resolve } from 'node:path'
|
|
40
|
+
import { fileURLToPath } from 'node:url'
|
|
41
|
+
import { isDirectRun } from './lib/is-direct-run.mjs'
|
|
42
|
+
|
|
43
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
44
|
+
const SELF_ROOT = resolve(HERE, '..')
|
|
45
|
+
|
|
46
|
+
/** 内部清单的相对路径(**只此一处**;改名要同时改门禁、安装器、文档)。 */
|
|
47
|
+
export const INTERNAL_MANIFEST = 'skills/internal.json'
|
|
48
|
+
/** 技能目录下必须存在的入口文件(`dsh-skill-filesystem#parseSkillFile` 的硬约束之一)。 */
|
|
49
|
+
export const SKILL_FILE = 'SKILL.md'
|
|
50
|
+
|
|
51
|
+
const readJson = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return null } }
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 归类(**唯一实现**;纯函数式:只读传入的 root)。
|
|
55
|
+
* @param {{root?:string}} [opts]
|
|
56
|
+
* @returns {{ok:boolean, external:string[], internal:string[], unclassified:string[],
|
|
57
|
+
* nonSkillDirs:string[], errors:Array<{rule:string,message:string}>, warnings:Array<{rule:string,message:string}>,
|
|
58
|
+
* manifest:object|null, sources:Array<{vertical:string,skills:string[]}>}}
|
|
59
|
+
*/
|
|
60
|
+
export function classify({ root = SELF_ROOT } = {}) {
|
|
61
|
+
const errors = []
|
|
62
|
+
const warnings = []
|
|
63
|
+
const skillsRoot = join(root, 'skills')
|
|
64
|
+
|
|
65
|
+
// ── G3:外发集 = 所有 verticals/*/manifest.json#skills 的并集 ────────────────
|
|
66
|
+
const verticalsRoot = join(root, 'verticals')
|
|
67
|
+
const sources = []
|
|
68
|
+
const external = []
|
|
69
|
+
let verticalDirs = []
|
|
70
|
+
try {
|
|
71
|
+
verticalDirs = readdirSync(verticalsRoot, { withFileTypes: true })
|
|
72
|
+
.filter((e) => e.isDirectory() && existsSync(join(verticalsRoot, e.name, 'manifest.json')))
|
|
73
|
+
.map((e) => e.name).sort()
|
|
74
|
+
} catch { verticalDirs = [] }
|
|
75
|
+
if (!verticalDirs.length) {
|
|
76
|
+
errors.push({ rule: 'G3', message: `读不到任何 verticals/*/manifest.json(外发集无从得出):${verticalsRoot}` })
|
|
77
|
+
}
|
|
78
|
+
for (const v of verticalDirs) {
|
|
79
|
+
const man = readJson(join(verticalsRoot, v, 'manifest.json'))
|
|
80
|
+
if (!man) { errors.push({ rule: 'G3', message: `verticals/${v}/manifest.json 不可解析` }); continue }
|
|
81
|
+
const skills = Array.isArray(man.skills) ? man.skills.map(String) : []
|
|
82
|
+
if (!skills.length) warnings.push({ rule: 'G3', message: `verticals/${v}/manifest.json#skills 为空(该业务线不声明任何流程技能?)` })
|
|
83
|
+
sources.push({ vertical: v, skills })
|
|
84
|
+
for (const s of skills) if (!external.includes(s)) external.push(s)
|
|
85
|
+
}
|
|
86
|
+
external.sort()
|
|
87
|
+
|
|
88
|
+
// ── G1:内部清单必须存在且可解析 ──────────────────────────────────────────
|
|
89
|
+
const manPath = join(root, INTERNAL_MANIFEST)
|
|
90
|
+
const manifest = existsSync(manPath) ? readJson(manPath) : null
|
|
91
|
+
const internalMap = manifest && manifest.internal && typeof manifest.internal === 'object' ? manifest.internal : null
|
|
92
|
+
if (!existsSync(manPath)) {
|
|
93
|
+
errors.push({ rule: 'G1', message: `缺内部清单 ${INTERNAL_MANIFEST} —— 内部半边**必须**是清单文件(不许把规则写死在安装脚本里)` })
|
|
94
|
+
} else if (!manifest) {
|
|
95
|
+
errors.push({ rule: 'G1', message: `${INTERNAL_MANIFEST} 不是合法 JSON` })
|
|
96
|
+
} else if (!internalMap) {
|
|
97
|
+
errors.push({ rule: 'G1', message: `${INTERNAL_MANIFEST} 缺 \`internal\` 对象(形状见该文件 _doc)` })
|
|
98
|
+
}
|
|
99
|
+
const internal = internalMap ? Object.keys(internalMap).sort() : []
|
|
100
|
+
|
|
101
|
+
// ── skills/ 下的真实目录 ─────────────────────────────────────────────────
|
|
102
|
+
let entries = []
|
|
103
|
+
try { entries = readdirSync(skillsRoot, { withFileTypes: true }) } catch {
|
|
104
|
+
errors.push({ rule: 'G2', message: `读不到技能目录:${skillsRoot}` })
|
|
105
|
+
}
|
|
106
|
+
const skillDirs = []
|
|
107
|
+
const nonSkillDirs = []
|
|
108
|
+
for (const e of entries) {
|
|
109
|
+
if (!e.isDirectory() || e.name.startsWith('.')) continue
|
|
110
|
+
if (existsSync(join(skillsRoot, e.name, SKILL_FILE))) skillDirs.push(e.name)
|
|
111
|
+
else nonSkillDirs.push(e.name)
|
|
112
|
+
}
|
|
113
|
+
skillDirs.sort(); nonSkillDirs.sort()
|
|
114
|
+
|
|
115
|
+
// ── G2:未归类即红 ───────────────────────────────────────────────────────
|
|
116
|
+
const unclassified = skillDirs.filter((d) => !external.includes(d) && !internal.includes(d))
|
|
117
|
+
for (const d of unclassified) {
|
|
118
|
+
errors.push({
|
|
119
|
+
rule: 'G2',
|
|
120
|
+
message: `未归类:skills/${d}/ 既不在任何 verticals/*/manifest.json#skills(外发),也不在 ${INTERNAL_MANIFEST}(内部)`
|
|
121
|
+
+ ' —— 往 skills/ 丢目录 = 默认发给每个客户(升级器无角色过滤)⇒ 必须做一次决定',
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
for (const d of nonSkillDirs) {
|
|
125
|
+
warnings.push({ rule: 'G8', message: `skills/${d}/ 没有 ${SKILL_FILE} ⇒ 不算技能(不判红;若是废弃技能请删目录)` })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── G4/G5/G6/G7 ─────────────────────────────────────────────────────────
|
|
129
|
+
for (const s of external) {
|
|
130
|
+
if (!existsSync(join(skillsRoot, s, SKILL_FILE))) {
|
|
131
|
+
errors.push({ rule: 'G4', message: `业务线声明了技能 \`${s}\`,但 skills/${s}/${SKILL_FILE} 不存在(装机时会静默少一个)` })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const s of internal) {
|
|
135
|
+
if (!existsSync(join(skillsRoot, s, SKILL_FILE))) {
|
|
136
|
+
errors.push({ rule: 'G5', message: `${INTERNAL_MANIFEST} 列了 \`${s}\`,但 skills/${s}/${SKILL_FILE} 不存在(过期条目)` })
|
|
137
|
+
}
|
|
138
|
+
const reason = internalMap[s] && (internalMap[s].reason || internalMap[s]._reason)
|
|
139
|
+
if (!reason || !String(reason).trim()) {
|
|
140
|
+
errors.push({ rule: 'G6', message: `${INTERNAL_MANIFEST} 的 \`${s}\` 缺 reason(没有理由的"内部"半年后没人敢删)` })
|
|
141
|
+
}
|
|
142
|
+
if (external.includes(s)) {
|
|
143
|
+
errors.push({ rule: 'G7', message: `\`${s}\` 同时出现在外发集(业务线声明)与内部清单里 ⇒ 归类矛盾` })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
ok: errors.length === 0,
|
|
149
|
+
root, external, internal, unclassified, nonSkillDirs,
|
|
150
|
+
errors, warnings, manifest, sources,
|
|
151
|
+
counts: { external: external.length, internal: internal.length, skillDirs: skillDirs.length, unclassified: unclassified.length },
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
156
|
+
function main(argv) {
|
|
157
|
+
let root = SELF_ROOT
|
|
158
|
+
let asJson = false
|
|
159
|
+
let strictWarn = false
|
|
160
|
+
for (let i = 0; i < argv.length; i++) {
|
|
161
|
+
const a = argv[i]
|
|
162
|
+
if (a === '--json') { asJson = true; continue }
|
|
163
|
+
if (a === '--strict-warn') { strictWarn = true; continue }
|
|
164
|
+
if (a === '--root') { const v = argv[i + 1]; if (!v || v.startsWith('--')) return { code: 2, out: '❌ --root 缺值' }; root = resolve(v); i++; continue }
|
|
165
|
+
if (a === '-h' || a === '--help') return { code: 0, out: readFileSync(fileURLToPath(import.meta.url), 'utf8').split('*/')[0].replace(/^\/\*\*?/, '').trim() }
|
|
166
|
+
return { code: 2, out: `❌ 未知参数:${a}(用法:node scripts/check-skill-classification.mjs [--root <dir>] [--strict-warn] [--json])` }
|
|
167
|
+
}
|
|
168
|
+
if (!existsSync(root)) return { code: 2, out: `❌ 根不存在:${root}` }
|
|
169
|
+
const r = classify({ root })
|
|
170
|
+
const bad = r.errors.length + (strictWarn ? r.warnings.length : 0)
|
|
171
|
+
if (asJson) return { code: bad ? 1 : 0, out: JSON.stringify({ ...r, strictWarn, code: bad ? 1 : 0 }, null, 2) }
|
|
172
|
+
const lines = ['', '【技能归类门禁(P3 / §6.1 D5)】']
|
|
173
|
+
lines.push(` 外发 ${r.counts.external} 个(真源:${r.sources.map((s) => `verticals/${s.vertical}/manifest.json#skills`).join('、') || '(无)'})`)
|
|
174
|
+
for (const s of r.sources) lines.push(` · ${s.vertical}: ${s.skills.join('、') || '(空)'}`)
|
|
175
|
+
lines.push(` 内部 ${r.counts.internal} 个(${INTERNAL_MANIFEST})`)
|
|
176
|
+
for (const s of r.internal) lines.push(` · ${s} —— ${r.manifest.internal[s].reason || '(缺理由)'}`)
|
|
177
|
+
for (const e of r.errors) lines.push(` ❌ [${e.rule}] ${e.message}`)
|
|
178
|
+
for (const w of r.warnings) lines.push(` ⚠️ [${w.rule}] ${w.message}`)
|
|
179
|
+
lines.push(` 结论:${r.ok ? '✅ 通过' : '❌ 不通过'}(外发 ${r.counts.external} / 内部 ${r.counts.internal} / 未归类 ${r.counts.unclassified})`
|
|
180
|
+
+ `${strictWarn ? `(--strict-warn:${r.warnings.length} 条提示也计入)` : ''}`)
|
|
181
|
+
return { code: bad ? 1 : 0, out: lines.join('\n') }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (isDirectRun(import.meta.url)) {
|
|
185
|
+
const { code, out } = main(process.argv.slice(2))
|
|
186
|
+
console.log(out)
|
|
187
|
+
process.exit(code)
|
|
188
|
+
}
|