@thinkingai/ae-cli 6.1.18 → 6.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +2 -0
  2. package/README.zh.md +6 -0
  3. package/dist/{auth-ROB2EDYV.js → auth-QPEVVBKT.js} +13 -7
  4. package/dist/auth-UMJAA72O.js +15 -0
  5. package/dist/{capability-DKMYUTLC.js → capability-ASI62I4A.js} +11 -10
  6. package/dist/{chunk-UW5UN47B.js → chunk-533JK3YX.js} +2 -0
  7. package/dist/{chunk-4XXOWOTA.js → chunk-6QK7SYYY.js} +5 -4
  8. package/dist/{chunk-TUKQZTMI.js → chunk-7K24F7T2.js} +2 -0
  9. package/dist/{chunk-OO6XR6LK.js → chunk-AMBFK2K3.js} +2 -0
  10. package/dist/{sync-HKIOZXQE.js → chunk-B5HMQAOM.js} +31 -540
  11. package/dist/{chunk-Y3LOALAV.js → chunk-DKEXH6Q5.js} +4 -3
  12. package/dist/{chunk-VTWMIC5L.js → chunk-E2JKXMVO.js} +2 -0
  13. package/dist/{chunk-JHENBQ5B.js → chunk-FR2EDAQM.js} +1 -0
  14. package/dist/{chunk-ZQ47LWTI.js → chunk-GJXRF7Y6.js} +6 -5
  15. package/dist/{chunk-DWO43OIB.js → chunk-JQ3ENZZH.js} +2 -0
  16. package/dist/chunk-JYMGHGCE.js +191 -0
  17. package/dist/{chunk-BYYS3ANB.js → chunk-KEWVSO64.js} +5 -4
  18. package/dist/{chunk-3FY3RJ26.js → chunk-KZ6ZHF5J.js} +2 -1
  19. package/dist/{chunk-ZQKDZXDO.js → chunk-O6K2ODUE.js} +3 -2
  20. package/dist/chunk-QL6R6TSK.js +25215 -0
  21. package/dist/{chunk-J2DEBMRF.js → chunk-RWXIXOZG.js} +22 -9
  22. package/dist/{chunk-QZ3AS4KK.js → chunk-S5PONFBM.js} +5 -4
  23. package/dist/{chunk-4NN5IWVN.js → chunk-T6OSFQZD.js} +2 -0
  24. package/dist/{chunk-3KWQYGYI.js → chunk-TKHSULJT.js} +2 -0
  25. package/dist/{chunk-6EIJSNBD.js → chunk-Y74WTIKJ.js} +2 -0
  26. package/dist/{client-L2YDMHQ6.js → client-N7NGZUNI.js} +4 -3
  27. package/dist/{community-report-client-C7WDGET3.js → community-report-client-6OL6GBQW.js} +3 -2
  28. package/dist/{config-BMYZX2UE.js → config-XNMACCKX.js} +8 -7
  29. package/dist/{data-integration-QEKDWQDY.js → data-integration-4NZGZ45E.js} +913 -92
  30. package/dist/index.js +38 -36
  31. package/dist/{local-data-upload-client-4YYHSYD6.js → local-data-upload-client-EFHDLXFG.js} +3 -2
  32. package/dist/{memory-I2WXDTV2.js → memory-HBJQ2GC5.js} +8 -7
  33. package/dist/{metadata-I4C2EWUN.js → metadata-62EWBEIG.js} +12 -11
  34. package/dist/{model-HLHIEFMU.js → model-BHLJ2QMT.js} +10 -9
  35. package/dist/{personal-semantic-preference-LIPACBDX.js → personal-semantic-preference-JBVMAI7L.js} +11 -10
  36. package/dist/{project-semantic-RT3R2VQD.js → project-semantic-BQ4MPUS5.js} +12 -11
  37. package/dist/sync-MUWPRM5I.js +522 -0
  38. package/dist/{te-agent-BR6VDBNX.js → te-agent-WQZTMEK4.js} +10 -9
  39. package/dist/{te-analysis-7VUNUYWZ.js → te-analysis-MNCG65LU.js} +107 -20
  40. package/dist/{te-community-5DMNKJWY.js → te-community-BNE6KREL.js} +8 -7
  41. package/dist/{te-dataops-6P5IKWNJ.js → te-dataops-TXLKM2W6.js} +467 -67
  42. package/dist/{te-engage-KZPR5R22.js → te-engage-TPKLRVTL.js} +84 -10
  43. package/dist/{te-experiment-6BITX4RD.js → te-experiment-6BYKTCY7.js} +85 -12
  44. package/dist/{te-kb-RCLSSH2Q.js → te-kb-BUD6N3NI.js} +311 -24
  45. package/dist/{te-system-FXITO2JG.js → te-system-44LZJDTQ.js} +573 -37
  46. package/dist/{te-team-ADOC2ROP.js → te-team-XMP47YXE.js} +10 -7
  47. package/dist/{update-YCYCKJOO.js → update-UODFOZ4P.js} +7 -6
  48. package/package.json +18 -9
  49. package/skills/ae-analysis/references/analysis_gateway_assets.md +1 -1
  50. package/skills/ae-analysis/references/audience_models.md +18 -0
  51. package/skills/ae-analysis/references/command_index.md +2 -2
  52. package/skills/ae-analysis/references/project_timezone_update.md +13 -4
  53. package/skills/ae-analysis/references/property_create.md +2 -0
  54. package/skills/ae-analysis/references/super_metadata_batch_create.md +2 -0
  55. package/skills/ae-analysis/references/user_cluster_models.md +2 -0
  56. package/skills/ae-analysis/references/user_cluster_update.md +8 -4
  57. package/skills/ae-analysis/references/user_tag_create.md +7 -1
  58. package/skills/ae-analysis/references/user_tag_models.md +8 -2
  59. package/skills/ae-data-integration/SKILL.md +1 -0
  60. package/skills/ae-data-integration/references/error-handling.md +54 -1
  61. package/skills/ae-data-integration/references/source-inspect.md +18 -2
  62. package/skills/ae-data-integration/references/transform.md +3 -1
  63. package/skills/ae-data-integration/references/ue-routing.md +23 -0
  64. package/skills/ae-dataops/SKILL.md +11 -1
  65. package/skills/ae-dataops/references/dataops-backfill.md +135 -0
  66. package/skills/ae-engage/SKILL.md +5 -0
  67. package/skills/ae-engage/references/build-task-save-guide.md +5 -1
  68. package/skills/ae-engage/references/save-flow.md +37 -1
  69. package/skills/ae-engage/references/save-task.md +6 -0
  70. package/skills/ae-experiment/SKILL.md +6 -2
  71. package/skills/ae-experiment/references/save_metric.md +20 -8
  72. package/skills/ae-kb/SKILL.md +120 -9
  73. package/skills/ae-system/SKILL.md +28 -30
  74. package/skills/ae-system/references/channel-management.md +303 -0
  75. package/dist/auth-GBMV6TEJ.js +0 -14
  76. package/dist/capability-HYVVPG25.js +0 -352
  77. package/dist/chunk-EFH4XWYC.js +0 -556
  78. package/dist/chunk-JRJY5DMJ.js +0 -71
  79. package/dist/chunk-OMPRXM3V.js +0 -349
  80. package/dist/chunk-QNOLN2LJ.js +0 -509
  81. package/dist/chunk-RJDU7NYP.js +0 -1198
  82. package/dist/chunk-RNAALWJK.js +0 -98
  83. package/dist/chunk-SERWF6G5.js +0 -13
  84. package/dist/memory-3ORCR7JH.js +0 -893
  85. package/dist/metadata-VUOQJE26.js +0 -339
  86. package/dist/model-UGRDX4MW.js +0 -139
  87. package/dist/personal-semantic-preference-OEISBRHM.js +0 -239
  88. package/dist/project-semantic-FFPWFPIW.js +0 -1114
  89. package/dist/sync-TFHU2UTG.js +0 -10261
  90. package/dist/te-agent-VLYOV7S4.js +0 -3894
  91. package/dist/te-analysis-4YGQL5RC.js +0 -9357
  92. package/dist/te-community-ISDQWJU7.js +0 -1859
  93. package/dist/te-dataops-CVULXNVB.js +0 -2209
  94. package/dist/te-engage-N5WI32H6.js +0 -4898
  95. package/dist/te-experiment-UVR4HLND.js +0 -988
  96. package/dist/te-system-K2GYMCTB.js +0 -2213
@@ -1,16 +1,19 @@
1
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
1
2
  import {
2
3
  getAuthHeaders,
3
4
  kbApi
4
- } from "./chunk-RNAALWJK.js";
5
+ } from "./chunk-JYMGHGCE.js";
5
6
  import {
6
7
  printError,
7
8
  printOutput
8
- } from "./chunk-BYYS3ANB.js";
9
- import "./chunk-J2DEBMRF.js";
10
- import "./chunk-ZQKDZXDO.js";
11
- import "./chunk-3FY3RJ26.js";
12
- import "./chunk-UW5UN47B.js";
13
- import "./chunk-JHENBQ5B.js";
9
+ } from "./chunk-KEWVSO64.js";
10
+ import "./chunk-GJXRF7Y6.js";
11
+ import "./chunk-Y74WTIKJ.js";
12
+ import "./chunk-RWXIXOZG.js";
13
+ import "./chunk-O6K2ODUE.js";
14
+ import "./chunk-KZ6ZHF5J.js";
15
+ import "./chunk-533JK3YX.js";
16
+ import "./chunk-FR2EDAQM.js";
14
17
 
15
18
  // src/commands/te-team/shared.ts
16
19
  var API_PREFIX = process.env.AE_API_PREFIX ?? "/agent";
@@ -1,3 +1,4 @@
1
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
1
2
  import {
2
3
  buildVersionInstallPlan,
3
4
  fetchCliConfig,
@@ -7,19 +8,19 @@ import {
7
8
  printError,
8
9
  printOutput,
9
10
  recordVersionSyncResult
10
- } from "./chunk-BYYS3ANB.js";
11
+ } from "./chunk-KEWVSO64.js";
11
12
  import {
12
13
  peekCliToken
13
- } from "./chunk-J2DEBMRF.js";
14
+ } from "./chunk-RWXIXOZG.js";
14
15
  import {
15
16
  missingAeHostHint
16
- } from "./chunk-ZQKDZXDO.js";
17
+ } from "./chunk-O6K2ODUE.js";
17
18
  import {
18
19
  getActiveHost,
19
20
  normalizeUrl
20
- } from "./chunk-3FY3RJ26.js";
21
- import "./chunk-UW5UN47B.js";
22
- import "./chunk-JHENBQ5B.js";
21
+ } from "./chunk-KZ6ZHF5J.js";
22
+ import "./chunk-533JK3YX.js";
23
+ import "./chunk-FR2EDAQM.js";
23
24
 
24
25
  // src/commands/update.ts
25
26
  var HOST_OPTION_DESC = "Override active AE host URL (e.g., https://ta.thinkingdata.cn)";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkingai/ae-cli",
3
- "version": "6.1.18",
3
+ "version": "6.1.19",
4
4
  "description": "CLI tool for ThinkingAI (AE) analytics platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "dev": "tsx src/index.ts",
20
- "build": "tsup src/index.ts --format esm --outDir dist && node scripts/copy-tracking-assets.mjs",
20
+ "build": "tsup src/index.ts --format esm --outDir dist --clean --config tsup.config.ts && node scripts/copy-tracking-assets.mjs",
21
21
  "generate:analysis-skill": "tsx scripts/generate-analysis-skill-index.mts",
22
22
  "verify:config": "node test/config-commands.test.mjs",
23
23
  "verify:json-utils": "npx tsx tests/json-utils.test.ts",
@@ -42,15 +42,17 @@
42
42
  "verify:agent-tools": "node --test test/agent-command-registry.test.mjs && tsx tests/te-agent-credentials.test.ts && tsx tests/te-agent-unified-auth.test.ts && tsx tests/te-agent-conversations.test.ts && tsx scripts/verify-agent-tools.mjs && npm run verify:approval && npm run verify:sandbox-tools",
43
43
  "verify:approval": "tsx tests/approval-command.test.ts && tsx tests/approval-cli-client.test.ts && tsx tests/approval-skill.test.ts",
44
44
  "verify:memory": "tsx tests/memory-command.test.ts && tsx tests/memory-cli-client.test.ts && tsx tests/memory-skill.test.ts",
45
- "verify:system-tools": "node scripts/verify-system-tools.mjs && npx tsx tests/system-domain.test.ts && npx tsx tests/te-agent-download.test.ts && npx tsx tests/sandbox-tool-package.test.ts",
45
+ "verify:system-tools": "node scripts/verify-system-tools.mjs && npx tsx tests/system-domain.test.ts && npx tsx tests/channel-cli-http.test.ts && npx tsx tests/te-agent-download.test.ts && npx tsx tests/sandbox-tool-package.test.ts",
46
46
  "verify:kb-skill": "node test/kb-skill-query-workflow.test.mjs",
47
+ "verify:kb-sources": "tsx test/kb-list-sources-command.test.mjs",
47
48
  "verify:sandbox-tools": "tsx test/sandbox-tools.test.ts",
48
49
  "verify:skill-version-management": "tsx tests/skill-version-management.test.ts",
49
50
  "verify:skill-reference-files": "tsx tests/skill-reference-files.test.ts",
50
- "verify:tracking-tools": "tsx test/tracking-client.test.mjs && tsx test/tracking-skill-flow.test.mjs && node test/tracking-commands.test.mjs && tsx tests/tracking-display-name-sync.test.ts && tsx tests/local-data-inspect-convert.test.ts && tsx tests/local-data-upload-client.test.ts && tsx tests/local-data-upload-command.test.ts && tsx tests/local-data-encoding-sniff.test.ts && tsx tests/local-data-time-formats.test.ts && tsx tests/local-data-eight-types.test.ts && tsx tests/local-data-mapping-extras.test.ts && tsx tests/local-data-plan.test.ts && tsx tests/local-data-handoff.test.ts && tsx tests/local-data-reuse.test.ts && tsx tests/local-data-conflicts-resolutions.test.ts && tsx tests/runner-variadic-flags.test.ts && node test/local-data-skill.test.mjs",
51
+ "verify:tracking-tools": "tsx test/tracking-client.test.mjs && tsx test/tracking-skill-flow.test.mjs && node test/tracking-commands.test.mjs && tsx tests/tracking-display-name-sync.test.ts && tsx tests/local-data-inspect-convert.test.ts && tsx tests/local-data-upload-client.test.ts && tsx tests/local-data-upload-command.test.ts && tsx tests/local-data-encoding-sniff.test.ts && tsx tests/local-data-time-formats.test.ts && tsx tests/local-data-xlsx-dates.test.ts && tsx tests/local-data-xlsx-hidden-sheets.test.ts && tsx tests/local-data-xlsx-formulas.test.ts && tsx tests/local-data-title-rows.test.ts && tsx tests/local-data-xlsx-merged-cells.test.ts && tsx tests/local-data-xlsx-shared-strings.test.ts && tsx tests/local-data-xlsx-namespace-prefix.test.ts && tsx tests/local-data-value-distribution.test.ts && tsx tests/local-data-summary-rows.test.ts && tsx tests/local-data-duplicate-keys.test.ts && tsx tests/local-data-row-balance.test.ts && tsx tests/local-data-eight-types.test.ts && tsx tests/local-data-mapping-extras.test.ts && tsx tests/local-data-plan.test.ts && tsx tests/local-data-handoff.test.ts && tsx tests/local-data-reuse.test.ts && tsx tests/local-data-conflicts-resolutions.test.ts && tsx tests/runner-variadic-flags.test.ts && node test/local-data-skill.test.mjs",
51
52
  "verify:self-check-overlay": "node test/self-check-overlay.test.mjs",
52
53
  "verify:update-check": "npx tsx test/update-check.test.mjs",
53
54
  "verify:version-sync": "npx tsx tests/version-sync.test.ts",
55
+ "verify:dependency-hygiene": "node --test test/dependency-hygiene.test.mjs",
54
56
  "qa-changed": "node scripts/qa-changed.mjs",
55
57
  "self-check": "node self-check/scan.mjs",
56
58
  "check:release": "node self-check/release-gate.mjs",
@@ -59,7 +61,7 @@
59
61
  "check:agents-docs": "node scripts/check-agents-docs-sync.mjs",
60
62
  "prepublishOnly": "npm run check:release && npm run build",
61
63
  "start": "node dist/index.js",
62
- "test": "npx tsx src/index.ts --help && npm run verify:retired-api-command && npm run verify:sandbox-tools"
64
+ "test": "npx tsx src/index.ts --help && npm run verify:retired-api-command && npm run verify:sandbox-tools && npm run verify:dependency-hygiene"
63
65
  },
64
66
  "repository": {
65
67
  "type": "git",
@@ -77,11 +79,10 @@
77
79
  },
78
80
  "dependencies": {
79
81
  "@types/json-bigint": "^1.0.4",
80
- "archiver": "^5.3.2",
82
+ "archiver": "^8.0.0",
81
83
  "cli-table3": "^0.6.5",
82
84
  "commander": "^12.1.0",
83
85
  "csv-parse": "^7.0.2",
84
- "exceljs": "^4.4.0",
85
86
  "iconv-lite": "^0.7.3",
86
87
  "jq-wasm": "^3.0.0-jq-1.8.2",
87
88
  "jschardet": "^3.1.4",
@@ -95,16 +96,24 @@
95
96
  "xlsx": "^0.18.5"
96
97
  },
97
98
  "devDependencies": {
98
- "@types/archiver": "^5.3.4",
99
- "@types/iconv-lite": "^0.0.1",
99
+ "@types/archiver": "^8.0.0",
100
100
  "@types/node": "^22.0.0",
101
101
  "@types/stream-json": "^1.7.8",
102
102
  "@types/turndown": "^5.0.6",
103
103
  "@types/unzipper": "^0.10.11",
104
104
  "@types/ws": "^8.5.0",
105
+ "exceljs": "^4.4.0",
105
106
  "jszip": "^3.10.1",
106
107
  "tsup": "^8.0.0",
107
108
  "tsx": "^4.0.0",
108
109
  "typescript": "^5.5.0"
110
+ },
111
+ "overrides": {
112
+ "exceljs": {
113
+ "archiver": "8.0.0",
114
+ "fast-csv": "5.0.7",
115
+ "unzipper": "0.12.5",
116
+ "uuid": "11.1.1"
117
+ }
109
118
  }
110
119
  }
@@ -145,7 +145,7 @@ Prefer the run/artifact commands over hand-written HTTP, Python, or curl. Analys
145
145
  | `user-cluster-member list` | `analysis.user_cluster_member.list` | Bounded inline cluster members | `--cluster-name`, optional properties/fields/query/limit/offset | Member rows |
146
146
  | `user-cluster-member export` | `analysis.user_cluster_member.export` | Stream native full cluster members as csv.gz | `--cluster-name`, optional properties | Async artifact descriptor |
147
147
  | `user-cluster create` | `analysis.user_cluster.create` | Create condition/sql cluster directly from semantic intent | `--cluster-name`, `--display-name`, `--definition-request` | Create result and canonical request |
148
- | `user-cluster update` | `analysis.user_cluster.update` | Update condition/sql cluster | `--cluster-name`, fields to change, optional `--definition-request` | Update result |
148
+ | `user-cluster update` | `analysis.user_cluster.update` | Update condition/sql cluster after confirmation without changing its type or analysis entity | `--cluster-name`, fields to change, optional `--definition-request` with the existing type, then `--yes` after dry-run and explicit confirmation | Update result |
149
149
  | `user-cluster create-id` | `analysis.user_cluster.create_id` | Map imported values to an entity and create a cluster | `--display-name`, `--entity-id`, exactly one input source, conditional `--association-property` | Processing state; poll get for final match summary |
150
150
  | `user-cluster update-id` | `analysis.user_cluster.update_id` | Remap imported values for an ID cluster | `--cluster-name`, exactly one input source, conditional `--association-property` | Processing state; poll get for final match summary |
151
151
  | `user-cluster refresh` | `analysis.user_cluster.refresh` | Trigger cluster recompute | `--cluster-name` | Refresh result |
@@ -25,12 +25,30 @@ A property is either a name or a typed object:
25
25
 
26
26
  ## Time range
27
27
 
28
+ Use semantic `mode`, `unit`, and `value` fields. Always pass `unit` with `recent` or `previous`; the supported units are `day`, `week`, `month`, `quarter`, and `year`.
29
+
30
+ | User intent | `time_range` |
31
+ |---|---|
32
+ | Today | `{"mode":"recent","unit":"day","value":1}` |
33
+ | This week | `{"mode":"recent","unit":"week","value":1}` |
34
+ | This month | `{"mode":"recent","unit":"month","value":1}` |
35
+ | This quarter | `{"mode":"recent","unit":"quarter","value":1}` |
36
+ | This year | `{"mode":"recent","unit":"year","value":1}` |
37
+ | Yesterday | `{"mode":"previous","unit":"day","value":1}` |
38
+ | Previous month | `{"mode":"previous","unit":"month","value":1}` |
39
+ | From a fixed date through today | `{"mode":"start_to_today","start_time":"2026-07-01"}` |
40
+ | From a fixed date through yesterday | `{"mode":"start_to_yesterday","start_time":"2026-07-01"}` |
41
+
42
+ Use `custom` only when both boundaries are fixed:
43
+
28
44
  ```json
29
45
  {"mode":"recent","unit":"day","value":7}
30
46
  {"mode":"previous","unit":"day","value":30}
31
47
  {"mode":"custom","start_time":"2026-07-01","end_time":"2026-07-07"}
32
48
  ```
33
49
 
50
+ Do not pass backend `recent_day` encodings inside `time_range`; the capability compiles the semantic object to the existing analysis representation.
51
+
34
52
  ## Filter group
35
53
 
36
54
  Groups use `{relation,items}`. Each item has `field`, `operator`, and optional `values`:
@@ -113,7 +113,7 @@ This is the exhaustive command and flag inventory for the analysis skill. Read t
113
113
  | `ae-cli analysis user-cluster get` | analysis.user_cluster.get | read | `--project-id` (number; required) — Numeric project ID.<br>`--cluster-names` (json; required) — JSON array of exact cluster_name values. | [user_cluster_get.md](user_cluster_get.md) |
114
114
  | `ae-cli analysis user-cluster list` | analysis.user_cluster.list | read | `--project-id` (number; required) — Numeric project ID.<br>`--queries` (json; optional) — Optional JSON array of 1 to 20 keyword filters. Results match any keyword.<br>`--fields` (json; optional) — Optional result field projection JSON array.<br>`--limit` (number; optional, min=1, max=200) — Directory page size. Default: 50, max: 200. Values outside 1..200 are rejected.<br>`--offset` (number; optional, min=0) — Zero-based directory page offset. Default: 0. Negative values are rejected.<br>`--authenticated-only` (boolean; optional) — Return only resources whose metadata can be resolved under the current identity. | [user_cluster_list.md](user_cluster_list.md) |
115
115
  | `ae-cli analysis user-cluster refresh` | analysis.user_cluster.refresh | write | `--project-id` (number; required) — Numeric project ID.<br>`--cluster-name` (string; required) — Exact cluster_name. Discover real values with analysis user-cluster list first. | [user_cluster_refresh.md](user_cluster_refresh.md) |
116
- | `ae-cli analysis user-cluster update` | analysis.user_cluster.update | write | `--project-id` (number; required) — Numeric project ID.<br>`--cluster-name` (string; required) — Exact cluster_name. Discover real values with analysis user-cluster list first.<br>`--display-name` (string; optional, minLength=1, maxLength=80) — Optional new display name. Maximum: 80 characters.<br>`--definition-request` (json; optional) — Optional semantic snake_case cluster definition. Read user_cluster_models.md; raw filts, C-codes, ftv, columnName, and backend DTOs are rejected.<br>`--authenticated-only` (boolean; optional) — Return only resources whose metadata can be resolved under the current identity.<br>`--remark` (string; optional, maxLength=400) — Optional new remark. Maximum: 400 characters.<br>`--zone-offset` (number; optional) — Optional timezone offset. UTC+8 is 8; UTC-5 is -5.<br>`--auto-refresh-cron` (string; optional) — Optional Quartz cron expression for an existing enabled auto-refresh schedule. This does not enable auto refresh. | [user_cluster_update.md](user_cluster_update.md) |
116
+ | `ae-cli analysis user-cluster update` | analysis.user_cluster.update | high-risk-write | `--project-id` (number; required) — Numeric project ID.<br>`--cluster-name` (string; required) — Exact cluster_name. Discover real values with analysis user-cluster list first.<br>`--display-name` (string; optional, minLength=1, maxLength=80) — Optional new display name. Maximum: 80 characters.<br>`--definition-request` (json; optional) — Optional semantic snake_case cluster definition. Its type must match the existing cluster type; update cannot change the analysis entity. Read user_cluster_models.md; raw filts, C-codes, ftv, columnName, and backend DTOs are rejected.<br>`--authenticated-only` (boolean; optional) — Return only resources whose metadata can be resolved under the current identity.<br>`--remark` (string; optional, maxLength=400) — Optional new remark. Maximum: 400 characters.<br>`--zone-offset` (number; optional) — Optional timezone offset. UTC+8 is 8; UTC-5 is -5.<br>`--auto-refresh-cron` (string; optional) — Optional Quartz cron expression for an existing enabled auto-refresh schedule. This does not enable auto refresh. | [user_cluster_update.md](user_cluster_update.md) |
117
117
  | `ae-cli analysis user-cluster update-id` | analysis.user_cluster.update_id | write | `--project-id` (number; required) — Numeric project ID.<br>`--cluster-name` (string; required) — Exact cluster_name. Discover real values with analysis user-cluster list first.<br>`--display-name` (string; optional, minLength=1, maxLength=80) — Optional new display name. Maximum: 80 characters.<br>`--input-file` (string; optional) — Local CSV path. The CLI uploads it with purpose analysis.user.id_import before executing the create/update capability.<br>`--input-file-id` (string; optional) — Reuse a file already uploaded for purpose analysis.user.id_import.<br>`--file-content` (string; optional) — Headerless UTF-8 CSV. No header row. Exactly one non-empty column per row: association-property value for the primary user entity, otherwise the entity ID.<br>`--remarks` (string; optional, maxLength=400) — Optional remarks. Maximum: 400 characters.<br>`--association-property` (string; optional) — Required only for the primary user entity. The first CSV column is matched against this allowed user property; #user_id is forbidden. Omit for non-primary entities. | [user_cluster_update_id.md](user_cluster_update_id.md) |
118
118
  | `ae-cli analysis user-cluster-member export` | analysis.user_cluster_member.export | read | `--cluster-name` (string; required) — Exact cluster_name. Discover real values with analysis user-cluster list first.<br>`--project-id` (number; required) — Numeric project ID.<br>`--property-names` (json; optional) — Optional JSON array of user property names to include.<br>`--request-id` (string; optional) — Optional caller-supplied cli_<32 lowercase hex> lifecycle ID. ae-cli generates and prints one before dispatch when omitted.<br>`--artifact-format` (string; optional) — Artifact format: jsonl or csv. Default: jsonl. Both formats use native full-download streaming and gzip compression.<br>`--timeout-seconds` (number; optional, min=1, max=21600) — Async runtime in seconds. Default and max: 21600 (6 hours); cancel earlier with analysis query cancel --run-id <run_id>.<br>`--wait` (boolean; optional) — Wait for the remote run and artifact to reach a terminal state. Polling uses short inspect requests; interrupting does not cancel the remote run.<br>`--wait-timeout-seconds` (number; optional, min=1, max=21600) — Maximum time this CLI process waits. Default: 600 seconds; expiry never cancels the remote run.<br>`--output` (string; optional) — Wait, then stream the completed artifact to this local file. Implies --wait.<br>`--force` (boolean; optional) — Allow --output to atomically replace an existing file. Without this flag, existing paths are refused. | [user_cluster_member_export.md](user_cluster_member_export.md) |
119
119
  | `ae-cli analysis user-cluster-member list` | analysis.user_cluster_member.list | read | `--cluster-name` (string; required) — Exact cluster_name. Discover real values with analysis user-cluster list first.<br>`--project-id` (number; required) — Numeric project ID.<br>`--property-names` (json; optional) — Optional JSON array of user property names to include.<br>`--fields` (json; optional) — Optional result fields. Defaults to #user_id, #account_id, and #distinct_id.<br>`--query` (string; optional) — Optional keyword filter.<br>`--use-cache` (boolean; optional) — Whether to use query cache. Default: true.<br>`--preview-rows` (number; optional, min=1, max=100000) — Maximum business rows returned per result. Default: 1000, matching the UI member query. Maximum: 100000.<br>`--request-id` (string; optional) — Optional caller-supplied cli_<32 lowercase hex> lifecycle ID. ae-cli generates and prints one before dispatch when omitted.<br>`--timeout-seconds` (number; optional, min=1, max=180) — Sync timeout seconds. Default: 120, max: 180. | [user_cluster_member_list.md](user_cluster_member_list.md) |
@@ -253,7 +253,7 @@ This is the exhaustive command and flag inventory for the analysis skill. Read t
253
253
  | `ae-cli project role-user list` | project.role_user.list | read | `--project-id` (number; required) — Numeric project ID.<br>`--role-name` (string; required) — Role name. | [project_role_user_list.md](project_role_user_list.md) |
254
254
  | `ae-cli project timezone get` | project.timezone.get | read | `--project-id` (number; required) — Numeric project ID. | [project_timezone_get.md](project_timezone_get.md) |
255
255
  | `ae-cli project timezone overview` | project.timezone.overview | read | `--project-id` (number; required) — Numeric project ID. | [project_timezone_overview.md](project_timezone_overview.md) |
256
- | `ae-cli project timezone update` | project.timezone.update | write | `--project-id` (number; required) — Numeric project ID.<br>`--payload` (json; required) — Required snake_case capability payload. Read the dedicated command reference for its semantic shape; an empty object is not a generic valid payload.<br>`--item` (string; required) — Timezone item: timezone_toggle, zone_offset, user_timezone, project_timezone_display. | [project_timezone_update.md](project_timezone_update.md) |
256
+ | `ae-cli project timezone update` | project.timezone.update | write | `--project-id` (number; required) — Numeric project ID.<br>`--payload` (json; required) — Item-specific snake_case payload: timezone_toggle uses {"toggle":true}; zone_offset uses {"column_name":"#zone_offset"}; user_timezone uses {"column_name":"user_timezone","codetable":"optional_table"}; project_timezone_display uses {"display_timezones":[{"timezone":8,"is_default":true}]}.<br>`--item` (string; required) — Timezone item: timezone_toggle, zone_offset, user_timezone, project_timezone_display. | [project_timezone_update.md](project_timezone_update.md) |
257
257
  | `ae-cli project user-id-items update` | project.user_id_items.update | write | `--project-id` (number; required) — Numeric project ID.<br>`--payload` (json; required) — Required snake_case capability payload. Read the dedicated command reference for its semantic shape; an empty object is not a generic valid payload. | [project_user_id_items_update.md](project_user_id_items_update.md) |
258
258
  | `ae-cli system admin list` | system.admin.list | read | `--company-id` (number; required, min=1) — Company ID. | [system_admin_list.md](system_admin_list.md) |
259
259
  | `ae-cli system admin remove` | system.admin.remove | high-risk-write | `--company-id` (number; required, min=1) — Company ID.<br>`--target-open-id` (string; required) — Target member open ID. | [system_admin_remove.md](system_admin_remove.md) |
@@ -7,13 +7,22 @@ Do not use it for unrelated project-management actions or for fields not present
7
7
  Command:
8
8
 
9
9
  ```bash
10
- ae-cli project timezone update --project-id <project_id> --payload <payload> --item <item>
11
- ae-cli project timezone update --dry-run --project-id <project_id> --payload <payload> --item <item>
10
+ ae-cli project timezone update --project-id <project_id> --item timezone_toggle --payload '{"toggle":true}'
11
+ ae-cli project timezone update --project-id <project_id> --item timezone_toggle --payload '{"toggle":true}' --dry-run
12
12
  ```
13
13
 
14
14
  Capability id: `project.timezone.update`.
15
15
 
16
- Input sends `project_id`, `payload`, `item`. Payload keys, JSON arrays, and projection fields must follow the common-service snake_case input schema.
16
+ Input sends `project_id`, `payload`, and `item`. The payload is selected by `item`; use exactly one of these snake_case shapes:
17
+
18
+ | `item` | `payload` | Notes |
19
+ |---|---|---|
20
+ | `timezone_toggle` | `{"toggle":true}` | `toggle` is required and boolean. Use `true` to enable multi-timezone and `false` to disable it. Never send `time_zone_enabled`. |
21
+ | `zone_offset` | `{"column_name":"#zone_offset"}` | `column_name` is required and selects the event property used as the timezone offset. |
22
+ | `user_timezone` | `{"column_name":"user_timezone","codetable":"timezone_code_table"}` | `column_name` is required. `codetable` is optional. |
23
+ | `project_timezone_display` | `{"display_timezones":[{"timezone":8,"is_default":true},{"timezone":0,"is_default":false}]}` | `display_timezones` is a non-empty array. `timezone` is an integer from -12 through 14, or 99 for no fixed display timezone; `is_default` is optional and boolean. |
24
+
25
+ Do not infer a payload field from the `project timezone get` output. In particular, its `time_zone_enabled` response field is not accepted by the update DTO; `timezone_toggle` always uses `payload.toggle`.
17
26
 
18
27
  Output uses the gateway envelope: success is `ok=true,data,meta`; failure is `ok=false,error`. Preserve `request_id` and `invocation_id` when present.
19
28
 
@@ -21,5 +30,5 @@ Output uses the gateway envelope: success is `ok=true,data,meta`; failure is `ok
21
30
  | Parameter | Required | Description |
22
31
  |---|---|---|
23
32
  | `--project-id` | Yes | Numeric project ID. |
24
- | `--payload` | Yes | Timezone update payload. Shape depends on item. |
33
+ | `--payload` | Yes | Item-specific payload from the table above. Do not send camelCase aliases or response-only fields. |
25
34
  | `--item` | Yes | Timezone item: timezone_toggle, zone_offset, user_timezone, project_timezone_display. |
@@ -13,6 +13,8 @@ ae-cli analysis-meta property create --dry-run
13
13
 
14
14
  Capability id: `metadata.property.create`.
15
15
 
16
+ Authorization requires the single project function permission `editSuperMeta` with the `metadata:write` scope. In the zh-CN permission UI, this permission is labeled `元数据管理 > 编辑`; the corresponding English label is `Metadata Management > Edit`. This is the same project permission used by `metadata.super_metadata.batch_create`. If either command returns `PROJECT_PERMISSION_DENIED`, do not retry with a different payload and do not describe the two capability IDs as two separate permissions. Ask a project administrator to grant this shared project permission to the current identity.
17
+
16
18
  Input sends `project_id`, `table_type`, `payload`.
17
19
 
18
20
  Output is a successful gateway envelope with no business data. Read back with `property get` using the same table type.
@@ -12,6 +12,8 @@ ae-cli analysis-meta super-metadata batch-create --project-id <project_id> --eve
12
12
 
13
13
  Capability id: `metadata.super_metadata.batch_create`.
14
14
 
15
+ Authorization requires the single project function permission `editSuperMeta` with the `metadata:write` scope. In the zh-CN permission UI, this permission is labeled `元数据管理 > 编辑`; the corresponding English label is `Metadata Management > Edit`. This is the same project permission used by `metadata.property.create`. If either command returns `PROJECT_PERMISSION_DENIED`, do not retry with a different payload and do not describe the two capability IDs as two separate permissions. Ask a project administrator to grant this shared project permission to the current identity.
16
+
15
17
  Input sends `project_id` plus any non-empty JSON arrays among `events`, `event_properties`, and `user_properties`. Use snake_case object fields exactly as documented by the common-service schema:
16
18
 
17
19
  - Event items: `event_name`, optional `event_desc`, optional `remark`, optional `super_event_prop_names`.
@@ -7,6 +7,8 @@ Top-level variants:
7
7
  - condition: `{"type":"condition","conditions":{"relation":"and|or","items":[...]}}`
8
8
  - SQL: `{"type":"sql","sql":"...","params":[{"name":"partdate","type":"part_date","recent_day":"1-7"}]}`
9
9
 
10
+ For `user-cluster update`, `type` must match the existing cluster type. The update command cannot change a cluster between condition and SQL, and it cannot change the cluster's analysis entity.
11
+
10
12
  For SQL clusters, quote Trino special identifiers with double quotes, for example `{"type":"sql","sql":"SELECT \"#user_id\" FROM v_user_1 WHERE vip_level >= 3"}`. This applies to identifiers containing `#`, `$`, `@`, spaces, or punctuation; single quotes are string literals.
11
13
 
12
14
  If a SQL cluster reads an event table, include a predicate on the quoted `"$part_date"` date-partition column; the backend rejects event-table SQL without it. This does not apply to the user-table example above.
@@ -2,18 +2,22 @@
2
2
 
3
3
  Update a condition or SQL user cluster. Discover the exact `cluster_name` first.
4
4
 
5
- Do not use it for ID-file membership replacement or to create a missing cluster. Supplying `--definition-request` automatically starts recomputation after the definition is updated; do not call `user-cluster refresh` afterward. Updating only `--display-name` or `--remark` does not recompute. `--auto-refresh-cron` changes an existing enabled auto-refresh schedule and does not enable auto refresh. A successful update means the definition was saved, not that the new result is complete. Poll `user-cluster get` until `progress=100` and `refresh_end_time` is not older than `update_time` before using `users_num` or querying members.
5
+ This is a `high-risk-write`. First run the final command with `--dry-run`, summarize the exact cluster and fields that will change, and wait for explicit user confirmation. Then run the unchanged command with `--yes`. Do not use it for ID-file membership replacement, to create a missing cluster, to change its analysis entity, or to change it between condition and SQL types.
6
+
7
+ Supplying `--definition-request` automatically starts recomputation after the definition is updated; do not call `user-cluster refresh` afterward. Updating only `--display-name` or `--remark` does not recompute. `--auto-refresh-cron` changes an existing enabled auto-refresh schedule and does not enable auto refresh. A successful update means the definition was saved, not that the new result is complete. Poll `user-cluster get` until `progress=100` and `refresh_end_time` is not older than `update_time` before using `users_num` or querying members.
6
8
 
7
9
  The response distinguishes both paths. A definition update returns `computation.triggered_automatically=true`, `result_freshness.is_stale=true`, and normally `next_action=poll_get` with an exact capability/input pair. A display-name/remark-only update returns `computation.status=not_triggered`, `result_freshness.status=fresh`, and `next_action=none`.
8
10
 
9
- Flags: `--project-id`, `--cluster-name` required. Optional: `--display-name`, `--definition-request`, `--authenticated-only`, `--remark`, `--zone-offset`, `--auto-refresh-cron`. The cluster type comes from `definition_request.type` when the definition changes.
11
+ Flags: `--project-id`, `--cluster-name` required. Optional: `--display-name`, `--definition-request`, `--authenticated-only`, `--remark`, `--zone-offset`, `--auto-refresh-cron`. There is no `--entity-id` update flag. When the definition changes, `definition_request.type` must match the existing cluster type; it selects the definition variant but does not change the saved type.
10
12
 
11
13
  `display_name` is at most 80 characters and `remark` is at most 400 characters. The CLI rejects violations before dispatch. `cluster_name` is an existing exact identifier and cannot be renamed by update.
12
14
 
13
15
  Read `user_cluster_models.md` before changing the definition. The backend validates and compiles `definition_request` inside update and refuses to modify the cluster if clarification is required. Condition definitions are saved as mixed-condition clusters.
14
16
 
15
17
  ```bash
16
- ae-cli analysis user-cluster update --project-id <project_id> --cluster-name retained_users --display-name "Retained Users v2"
18
+ ae-cli analysis user-cluster update --project-id <project_id> --cluster-name retained_users --display-name "Retained Users v2" --dry-run
19
+
20
+ ae-cli analysis user-cluster update --project-id <project_id> --cluster-name retained_users --display-name "Retained Users v2" --yes
17
21
 
18
- ae-cli analysis user-cluster update --project-id <project_id> --cluster-name retained_users --auto-refresh-cron '0 30 2 * * ? *'
22
+ ae-cli analysis user-cluster update --project-id <project_id> --cluster-name retained_users --auto-refresh-cron '0 30 2 * * ? *' --dry-run
19
23
  ```
@@ -10,10 +10,16 @@ Flags: `--project-id`, `--tag-name`, `--display-name`, `--definition-request` re
10
10
 
11
11
  `tag_name` is a machine identifier: 1-80 characters, starts with a letter, and contains only letters, digits, or underscores. `display_name` is 1-80 characters. The CLI rejects violations before dispatch.
12
12
 
13
- Read `user_tag_models.md` before constructing `--definition-request`. Create does not accept `--remark`; set it later with `user-tag update` when needed.
13
+ Read `user_tag_models.md` before constructing `--definition-request`. Dynamic first/last ranges use semantic `time_range` values such as `{"mode":"recent","unit":"month","value":1}` for this month or `{"mode":"start_to_today","start_time":"2026-07-01"}` for a fixed start date through today. Create does not accept `--remark`; set it later with `user-tag update` when needed.
14
14
 
15
15
  The backend validates and compiles the definition inside the create operation; if metadata is ambiguous or missing, creation fails without creating the tag.
16
16
 
17
17
  ```bash
18
18
  ae-cli analysis user-tag create --project-id <project_id> --tag-name high_value --display-name "High Value" --definition-request '{"type":"condition","condition_values":[{"value":"high","events":[{"event":"pay","operator":"gte","value":3,"aggregation":"count","time_range":{"mode":"recent","unit":"day","value":30}}]}]}'
19
19
  ```
20
+
21
+ First/last tag for this month:
22
+
23
+ ```bash
24
+ ae-cli analysis user-tag create --project-id <project_id> --tag-name latest_platform_this_month --display-name "Latest Platform This Month" --definition-request '{"type":"first_last","first_last":{"event":"login","occurrence":"last","property":"platform","time_range":{"mode":"recent","unit":"month","value":1}}}'
25
+ ```
@@ -32,10 +32,16 @@ Required: `event`, `aggregation`. `property`, `time_range`, and `filters` are op
32
32
 
33
33
  ## First/last tag
34
34
 
35
- Required: `event`, `occurrence=first|last`, and exactly one value source: `calculation` or `property`. `time_range` and `filters` are optional. Filters support only event properties and user properties. A string `field` is an event property; use `{name,type:"user_property"}` for a user property. Supplying neither or both value sources is rejected before execution.
35
+ Required: `event`, `occurrence=first|last`, and exactly one value source: `calculation` or `property`. `time_range` and `filters` are optional. Filters support only event properties and user properties. A string `field` is an event property; use `{name,type:"user_property"}` for a user property. Supplying neither or both value sources is rejected before execution. Use the semantic time mappings in [`audience_models.md`](audience_models.md) for dynamic ranges such as today, this month, or a fixed start date through today.
36
36
 
37
37
  ```json
38
- {"type":"first_last","first_last":{"event":"login","occurrence":"last","property":"platform","time_range":{"mode":"recent","unit":"day","value":30},"filters":{"relation":"and","items":[{"field":{"name":"country","type":"user_property"},"operator":"eq","values":["US"]}]}}}
38
+ {"type":"first_last","first_last":{"event":"login","occurrence":"last","property":"platform","time_range":{"mode":"recent","unit":"month","value":1},"filters":{"relation":"and","items":[{"field":{"name":"country","type":"user_property"},"operator":"eq","values":["US"]}]}}}
39
+ ```
40
+
41
+ From a fixed date through today:
42
+
43
+ ```json
44
+ {"type":"first_last","first_last":{"event":"login","occurrence":"first","calculation":"specific_time","time_range":{"mode":"start_to_today","start_time":"2026-07-01"}}}
39
45
  ```
40
46
 
41
47
  ## SQL tag
@@ -17,6 +17,7 @@ Two entrances lead here: the AE Agent dialog (attach / plus-button upload) and `
17
17
  - Do not invent account IDs, distinct IDs, event times, event names, projects, APPIDs, receivers, or timezones.
18
18
  - `value_mapping` and `random_pool` are explicit user decisions. Never invent them.
19
19
  - Never auto-fill a missing time for `track`/`track_*` rows. A missing time on user-profile rows may be filled with the current time only by setting `missing_time: 'now'` and only after the user explicitly confirms it.
20
+ - The mapping's fill-in options (`missing_time: 'now'`, `account_id_value`/`distinct_id_value`, `random_pool`, `exclude_columns`, `value_mapping`) record a decision the user has already made; they are never to make a validation failure disappear. When `convert` quarantines rows, present the failure first — the error `code`, the row count, its share of the total, and which events or time ranges are affected — and state the consequence in the user's terms ("3,000 events would carry a time that is not when they happened"). Only after the user has seen that may one of these options be set. When the evidence is insufficient, report the batch as not passed and pending customer data; never shrink the upload scope to manufacture a pass.
20
21
  - Do not read or send an AE access token or CLI token to `/sync_json`. The receiver request uses only APPID and UE data.
21
22
  - Never execute `data-integration upload` until the user has seen the target, mapping, valid/quarantined counts, batches, and dry-run and has explicitly confirmed that upload.
22
23
  - A blocked manifest requires a second, explicit clean-subset decision. Never add `--allow-clean-subset` implicitly.
@@ -52,7 +52,7 @@ the user decide — never to guess an encoding or structure and retry silently.
52
52
  | `LOCAL_DATA_INPUT_INVALID` | Generic parse failure (malformed CSV/TSV/JSON/XLS) | Verify encoding and structure, then retry without changing the source |
53
53
  | `LOCAL_DATA_JSONL_INVALID` | A JSONL line is not valid JSON (`location.record` names the line) | Point at the offending record |
54
54
  | `LOCAL_DATA_JSON_ROOT_INVALID` | JSON root is not an object or array | Check the file's top-level shape |
55
- | `LOCAL_DATA_XLSX_INVALID` | Workbook metadata missing / no readable sheets / worksheet entry missing | Re-export the workbook |
55
+ | `LOCAL_DATA_XLSX_INVALID` | Workbook metadata missing / no readable sheets / worksheet entry missing | Re-export the workbook (sheet recognition accepts `<sheet>` and namespaced `<x:sheet>` alike, so this is a real metadata gap) |
56
56
  | `LOCAL_DATA_SET_NOT_FOUND` / `LOCAL_DATA_SET_REQUIRED` | Sheet or JSON Path not found / ambiguous | Ask which `--data-set` to use |
57
57
 
58
58
  A parse error is never a reason to change the mapping or the tracking plan. Report the code and
@@ -81,6 +81,59 @@ Write failures (disk full `ENOSPC`, permission denied `EACCES`) do not hang the
81
81
  output streams fail with a clear `Failed to write "<path>"` message telling the user to check disk
82
82
  space and directory permissions. Fix the environment, then re-run into a fresh output directory.
83
83
 
84
+ ## File-level data-quality severity
85
+
86
+ The classes above are per row or per field. The user also needs one verdict for the file as a whole,
87
+ and its signals arrive scattered across the manifest and stderr. Grade them before reporting so the
88
+ same file gets the same verdict no matter who reports it.
89
+
90
+ | Severity | Signal | Handling |
91
+ | --- | --- | --- |
92
+ | Critical | Established with the user to be a cumulative snapshot or an aggregate report (see [ue-routing.md](ue-routing.md)); a required identity or time column empty for the whole file | Do not upload. Resolve with the user first |
93
+ | High | `manifest.output.invalid_records` is a large share of the row count; inspect reported `leading_title_rows` or `header_signal` and the user has not yet said whether those rows are a title/banner; inspect reported `xlsx_structure.merged_covered_cells` and the user has not yet said whether the merged label belongs on the rows the block covers; `summary_rows` was reported and the user has not yet said whether those rows are totals or real records; `duplicate_keys` was reported and the user has not yet said whether the repeated rows are separate observations | Upload is allowed, but name the item explicitly in the confirmation gate and in the completion response |
94
+ | Medium | `manifest.output.skipped_fields`; ragged delimited rows; `manifest.output.flatten_misses`; `manifest.output.unreadable_cells`; `xlsx_structure.hidden_rows` or `hidden_columns` read as data | Report the counts; the gate is unchanged |
95
+ | Low | `manifest.output.lan_ip_records` | Report once |
96
+
97
+ Rules:
98
+
99
+ - Severity is reported, never silently applied. A Critical finding stops the pipeline and is stated
100
+ as a data finding, not as a program error.
101
+ - Grade only what the run actually emitted. Every signal that belongs at High or Critical above now
102
+ has a detector, so grade it from what the run reported — never tell the user the tool checked
103
+ something it did not.
104
+ - `unreadable_cells` counts XLSX cells that carried no value the tool may use, grouped by cause:
105
+ `formula_no_cached_value` (the file stores a formula but not the result Excel last computed),
106
+ `error_value` (`#N/A`, `#DIV/0!`, …), `unreadable_object` (an unrecognized cell shape). The cells
107
+ read as missing and their rows are kept, so the record count says nothing about them — a column
108
+ that is empty in AE while the spreadsheet looks full is this. The tool never evaluates a formula
109
+ and never guesses a result; the fix is to recalculate and re-export in Excel, or to export values
110
+ instead of formulas. `inspect` reports the same counts before conversion.
111
+ - `xlsx_structure` records worksheet layout the rows themselves cannot carry: `merged_covered_cells`
112
+ counts cells that are empty only because a merged block covers them (Excel shows the value on the
113
+ block's first row), `hidden_rows` and `hidden_columns` name what the worksheet hides. The default
114
+ read changes none of it, so these counts describe what was uploaded: a column mostly missing in AE
115
+ while the spreadsheet looks full is the first of them. `merged_cells_filled` and
116
+ `excluded_hidden_rows` say what the run did about it, which is only ever what the user asked for
117
+ via `--fill-merged-cells` / `--exclude-hidden-rows` (mapping: `fill_merged_cells` /
118
+ `exclude_hidden_rows`). XLSX only — a legacy `.xls` workbook is not scanned.
119
+ - `summary_rows` names rows that read as a summary line rather than an observation, by `row` (the
120
+ data-row ordinal) and `signals`: `total_label` (a cell reads as `合计` / `总计` / `小计` / `汇总` / `Total` /
121
+ `Subtotal`) and `column_total` (a number equal to the total of its column's other rows). The rows
122
+ were converted like any other, so this is a finding about what was uploaded: one fabricated event
123
+ whose amount is the whole group's, and a column whose reported `sum` is twice its real total. There
124
+ is no flag that drops a data row, because a row labelled `合计` is sometimes a real record; the fix
125
+ is to remove it from the source file or re-export without it. Any format, `.xls` included.
126
+ - `duplicate_keys` names rows the source repeated under the same business key, by `key_columns` (the
127
+ columns compared), `duplicate_groups`, `extra_rows` (surplus records an upload would carry), and
128
+ `groups` with `count`, the data-row `rows`, and a `key_hash` prefix — never the key's own values.
129
+ Nothing was removed: a repeat is sometimes a real pair of records, two order lines in the same
130
+ checkout second, and AE appends accepted events with no way to un-send one, so ask the user whether
131
+ the rows are separate observations; if not, have them remove the rows from the source file. Values
132
+ are compared as written, so a repeat spelled two ways is missed, and `tracking_truncated` means
133
+ distinct keys outran the scan's budget and there may be more. Any format, `.xls` included.
134
+ - A large quarantine share has no fixed threshold. State the ratio and the dominant error `code`,
135
+ and let the user judge.
136
+
84
137
  ## Cross-cutting rules
85
138
 
86
139
  - Classify first, act second. Match on `code`, not on message text.
@@ -10,6 +10,8 @@ Confirm:
10
10
 
11
11
  Accept one or more CSV, TSV, TXT, JSON, JSONL (NDJSON), XLS, or XLSX files. CSV/TSV/TXT/JSON/JSONL/XLSX have no hard size limit; files over 1 GB print a stderr warning with an estimated processing time and suggest splitting. XLS over 100 MB prints a memory-risk warning (the legacy parser loads the whole workbook, roughly 5-10x file size); XLS over 1 GB is still rejected — convert it to XLSX or split it first.
12
12
 
13
+ XLSX worksheet recognition matches `<sheet>` and namespaced `<x:sheet>` alike: some cleaning/export tools rewrite the default OOXML namespace as a prefix, and the workbook reads the same either way. The worksheet *row* nodes themselves are still read unprefixed, so a workbook whose row data is also namespaced falls back to CSV.
14
+
13
15
  ## Inspect without exposing raw values
14
16
 
15
17
  Before the full inspection (which streams and profiles the entire file and can take
@@ -39,16 +41,30 @@ If `selection_required=true`, show only the Sheet/JSON Path candidates and ask t
39
41
  ae-cli data-integration inspect --input-file '<path>' --data-set '<candidate-id>' --source-timezone '<iana-timezone>'
40
42
  ```
41
43
 
42
- Report row/column counts, field types, missing/unique/time-parse ratios, UE eligibility, mapping confidence, and warnings. Samples are bounded (up to 5 distinct, truncated) — summarize, never paste them. ID-like columns (`id`, `*_id`, `*_key`, `*_code`, `*_no`, `*_num`) stay `string` even when every value is numeric; JSON-encoded object/array values inside CSV cells are recognized as `object`/`list`, not `string`. IP- and UUID-named columns are additionally checked against their value specs: inspect warns how many non-empty values are invalid IPv4/IPv6, private/LAN IPs, or non-UUID strings, so the user can decide whether to map them as `ip_field`/`uuid_field`. Read [UE routing](ue-routing.md) before choosing a branch.
44
+ Report row/column counts, field types, missing/unique/time-parse ratios, UE eligibility, mapping confidence, and warnings. Samples are bounded (up to 5 distinct, truncated) — summarize, never paste them. ID-like columns (`id`, `*_id`, `*_key`, `*_code`, `*_no`, `*_num`) stay `string` even when every value is numeric; JSON-encoded object/array values inside CSV cells are recognized as `object`/`list`, not `string`. IP- and UUID-named columns are additionally checked against their value specs: inspect warns how many non-empty values are invalid IPv4/IPv6, private/LAN IPs, or non-UUID strings, so the user can decide whether to map them as `ip_field`/`uuid_field`. Excel columns whose cells carry a date number format infer as `datetime` and are named in a warning (see **Excel date cells** below). Read [UE routing](ue-routing.md) before choosing a branch.
43
45
 
44
46
  A stderr `Warning: … column count different from the header row …` means the CSV/TSV has ragged rows (extra fields dropped, missing fields treated as empty); report it as a data-quality signal. For how every pipeline failure — abnormal data, parse errors, and program errors — is classified and handled, see [error handling](error-handling.md).
45
47
 
48
+ **Value frequency and numeric distribution.** A distinct count says how many different values a column holds, not whether they are worth uploading. Two per-column fields answer that, and both appear in `inspect` output only — never in the convert manifest:
49
+
50
+ - `value_frequency` — the 10 most frequent values as `{value, count, ratio}`, values truncated the way `samples` are and counted after truncation. It is reported only for a column whose distinct values all fit the tracked budget (200), so the counts are exact and complete when present; a column past the budget reports nothing here and its `unique_count` is the field to read instead. Use it to separate an enum from free text: a `渠道` column with three values is a property worth uploading, and its listed values are also what a `value_mapping` decision is made from. A single value covering every row usually means an export artifact, not data — propose `exclude_columns` and let the user decide.
51
+ - `numeric_summary` — `count`, `min`, `max`, `sum`, `mean`, `p25`, `median`, `p75` for a column that inferred as `number`. `count` is the values that read as numbers, which is below the column's non-missing count when the column is mixed. `count`/`min`/`max`/`sum`/`mean` are always exact; the quantiles come from a bounded sample on large columns and then `quantiles_approximate: true` says so. Use it to check the magnitude before it is locked into an AE property: a mean far below the maximum on a monotonically climbing column is the signature of a cumulative snapshot rather than a per-row measure (see [UE routing](ue-routing.md)), an amount whose values are 100× the expected size is a 分/元 unit mismatch, and a column that is entirely one number carries no signal.
52
+
53
+ Summarize both — report the shape of the distribution and the names of the values, and do not paste the whole table into the conversation. They are read out of the customer's file like `samples` are.
54
+
46
55
  ## Advanced input
47
56
 
48
57
  - **Headerless files** — inspect auto-detects a missing header row on CSV/TSV and reports `no_headers: true` with a `header_detection` verdict and `auto_headers` placeholders (`col_1..col_N`); the first row is already treated as data. `--headerless` forces the same behavior without detection. Never keep the `col_1..col_N` placeholders — they carry no business meaning. For each column, read its bounded samples and inferred type and propose a meaningful name, present every proposal to the user (column position, sample summary, suggested name), and let the user confirm or rename each one; record the confirmed names in the mapping's `headers` field. When the user already knows the names, re-run inspect with `--headers 'col1,col2,...'` so the recommended mapping carries them.
58
+ - **Title rows above the header** — an exported report often puts a caption in the first cell (`2026年3月销售明细`) and the real header row underneath. Read as-is, the caption becomes the file's only column name, every real column name is lost, and the header row is counted as a data row. Inspect reports the suspected rows under `leading_title_rows` (row ordinals and non-empty cell counts only, never the cell text) and warns — but it does **not** change what it read: the first row was still used as the header, because Excel exports legitimately carry numeric header rows (`2024`, `2025`) that look like data, and there is no flag that puts a header row back once it has been treated as data. So the report is a question for the user. When they confirm those rows are a title or banner, re-run inspect with `--skip-rows N` (N is exactly the last ordinal listed); the rerun reads the real header row, reports `skipped_rows`, and carries `skip_rows` into the recommended mapping so `convert` reads the same rows inspect profiled. On XLSX, inspect additionally reports `header_signal` when the row it used as the header looks like data — same rule: reported, not applied; resolve it with `--headers` or `--headerless`. This covers CSV/TSV and XLSX; a legacy `.xls` workbook accepts `--skip-rows` but is not scanned for title rows.
59
+ - **Summary and total rows** — an exported report ends with a 合计 row, and a grouped one repeats 小计 after every group. Those rows are not observations: uploaded, each becomes an event that never happened whose amount is the whole group's revenue, and profiled, it doubles the column's `sum` and turns its `max` into the total. Nothing in the row itself says so, so inspect flags them under `summary_rows` and warns. Each entry carries `row` (the data-row ordinal, the same numbering `invalid.rows.jsonl` and `--salvage-from` use) and `signals`: `total_label` when a cell reads as a total label (`合计` / `总计` / `小计` / `汇总` as a prefix, `Total` / `Subtotal` / `Sum` as the whole cell), naming the column in `label_column` but never the cell text; `column_total` when a number on that row equals the total of its column's other rows, listing those columns in `total_columns`. A row can raise one signal or both — a labelled group subtotal holds its group's total, not the column's, so only the label fires. Nothing is removed and nothing is changed: `convert` writes these rows as records, and every number reported for their columns counts them, which is exactly why the finding has to be read. There is no flag that drops a data row, because a row labelled `合计` is sometimes a real business record; when the user confirms a row is a total, ask them to remove it from the source file or re-export without it, then inspect again. `convert` repeats the finding in `manifest.output.summary_rows`, since by upload time a subtotal row with a plausible identity and time is indistinguishable from data. This covers every format, `.xls` included — the check reads rows, not worksheet structure.
60
+ - **Repeated business keys** — a customer re-exports a report whose range overlaps the last export, or pastes two sheets together, and the same observation arrives twice. Uploaded, each repeat is a second event: that user's revenue doubles, every funnel counts them twice, and AE appends accepted events with no way to un-send one — so the only place this is fixable is before the upload. Inspect compares each row's business key against the rows before it and reports repeats under `duplicate_keys`, plus a warning. The report carries `key_columns` (the columns actually compared — always read it, since the key is what the finding means), `checked_rows`, `duplicate_groups`, `extra_rows` (surplus records an upload would carry), and `groups`, each with `count`, the data-row `rows` (the numbering `invalid.rows.jsonl` and `--salvage-from` use), and a `key_hash` prefix that distinguishes groups without revealing values — the key's own text is never reported. `groups_truncated` / `rows_truncated` mean the list is bounded, not that the counts are; `tracking_truncated` means distinct keys outran the scan's budget and there may be more. The key comes from the mapping's identity, time, and event-name columns on `convert`, and from column-name matching on `inspect`; a single column is never a key, so a file with no recognizable time column is not scanned at all (identity alone would call every returning user's second row a repeat). Two limits to state when reporting: values are compared as written, so `2026-03-01 10:00:00` and `2026/03/01 10:00:00` are two different keys and a repeat spelled two ways is missed; and a source's own unique key (an order id) is not compared on unless the mapping names it as identity, time, or event. Nothing is removed — a repeat is sometimes a real pair of records, two order lines in the same checkout second — so ask the user whether the rows are separate observations, and if they are not, have them remove the rows from the source file and inspect again. `convert` repeats the finding in `manifest.output.duplicate_keys`, because by upload time both copies are ordinary valid records; it describes the whole source file even on a `--salvage-from` run, since every valid row of that file ends up in AE. This covers every format, `.xls` included — the check reads rows, not worksheet structure.
49
61
  - **TSV / TXT** — `.tsv` and `.tab` use a tab delimiter with no quoting convention; `.txt` and unknown extensions are content-sniffed into CSV, TSV, or NDJSON.
50
62
  - **Encoding** — text files are auto-detected (UTF-8, GBK, GB2312, Big5, and others); no flag is needed.
51
- - **Excel sheets** — `--merge-sheets` streams every worksheet in file order instead of a single selected sheet; otherwise ask which sheet/`--data-set` to use. Inspect also reports `header_consistency` (`all_same` or `different`) across a workbook's sheets, with `header_details` listing each sheet's header row when they differ; prefer `--merge-sheets` only when headers match.
63
+ - **Excel date cells** — a cell whose number format is a date or date+time is read as the wall-clock timestamp shown in Excel, not as the Excel serial number stored behind it, so the column infers as `datetime` and can serve as the time field. Inspect lists every such column in a warning. Treat that warning as a question to the user, not as a note: the same column profiled as `number` before this behavior existed, so if any part of this file was already sent to AE, the property may have been received as a number and its type is now locked — it cannot be changed to datetime, and the column has to be re-sent under a new property name. Ask whether the column was uploaded before, and only map it once the user answers. Elapsed-duration formats (`[h]:mm:ss` and the equivalent built-ins) are durations rather than points in time and stay `number`.
64
+ - **Excel formula cells** — a spreadsheet stores a formula and, next to it, the result Excel last computed. That cached result is the value: it is read normally, including a result of `0` or `""`, which are real values and not blanks. This tool never evaluates a formula and never guesses a result, so a cell holding a formula the file never computed has nothing to upload; it is read as missing and counted, as is an Excel error value (`#N/A`, `#DIV/0!`, …). Inspect reports the counts per column in a warning and `convert` repeats them in `manifest.output.unreadable_cells`. Report them: the rows are kept and the record count is unchanged, so this is the only explanation for a column that is empty in AE while the spreadsheet looks full. When a column that matters reads as missing this way, ask the user to recalculate and re-export in Excel, or to export values instead of formulas, before uploading. This covers XLSX; a legacy `.xls` workbook goes through a different parser and is not counted here.
65
+ - **Merged cells, hidden rows, and hidden columns** — a sheet maintained by hand merges a label down the rows it covers (`区域` spanning one region's block). Excel keeps that value on the block's first row only and stores every row below it as an empty cell, so a column that looks full on screen arrives mostly missing, and the AE property built from it would be empty for most events. The same worksheet may also hide a row inside a data block or hide a whole column. None of this travels with a row, so inspect scans the worksheet structure separately and reports it under `xlsx_structure`: `merged_ranges` with `merged_range_samples` (references such as `A3:A5`, never cell text), `merged_covered_cells` per column, `hidden_rows` with `hidden_row_samples` (source row numbers as Excel numbers them), and `hidden_columns` by header name. The default read is unchanged, so the report is a question for the user, and each answer is a flag: `--fill-merged-cells` copies each block's value into the cells its own range covers — bounded to the range, never overwriting a value that is there and never inventing one when the block's own cell is empty, so it is not a forward fill; `--exclude-hidden-rows` leaves hidden rows out. Neither is on by default: those cells really are empty in the file, and a row hidden inside a data block may still be real data — unlike a hidden *worksheet* (below), which is excluded by default. Hidden columns have no flag at all; when the user confirms one is not data, list it in the mapping's `exclude_columns`. Both flags are carried into the recommended mapping as `fill_merged_cells` / `exclude_hidden_rows`, which is what makes `convert` read the rows inspect profiled — `convert` has no read flags of its own — and `convert` repeats the findings in `manifest.output.xlsx_structure`, the only record of a layout the converted rows no longer show. This covers XLSX; a legacy `.xls` workbook is not scanned, so ask the user about merged labels and hidden rows there instead of trusting silence.
66
+ - **Hidden worksheets** — a worksheet hidden in the workbook is left out of the `--data-set` candidates and out of `--merge-sheets`, because a sheet the file does not show is usually scratch space, a lookup table, or a superseded draft rather than rows anyone meant to upload. Inspect lists each one under `excluded_sheets` (with `reason: hidden`); report those names to the user, since they are the only explanation for a row count lower than the workbook appears to hold. Their headers are also left out of `header_consistency`, so a stale hidden draft cannot make a mergeable workbook look ragged. A hidden sheet stays readable when the user names it in `--data-set` — the command then warns on stderr that the selected sheet is hidden. Only pass a hidden sheet after the user says that is what they want. When *every* worksheet is hidden there is no candidate left, and inspect fails with `LOCAL_DATA_ALL_DATA_SETS_HIDDEN` whose hint lists the hidden sheets; treat that as a question about which sheet holds the real data, not as an unreadable file. This detection covers XLSX only: a legacy `.xls` workbook's sheet list is unfiltered, so a hidden sheet there still appears as a candidate and is still merged — for `.xls`, ask the user to confirm the sheet list instead of trusting it.
67
+ - **Excel sheets** — `--merge-sheets` streams every visible worksheet in file order instead of a single selected sheet; otherwise ask which sheet/`--data-set` to use. Inspect also reports `header_consistency` (`all_same` or `different`) across a workbook's sheets, with `header_details` listing each sheet's header row when they differ; prefer `--merge-sheets` only when headers match. Matching headers establish a shared structure, not disjoint rows: a detail sheet and a summary sheet, or `1月` and `1月修订版`, usually carry identical headers and would be merged and reported twice over. Before merging, confirm with the user that the sources are mutually exclusive partitions (one month per sheet, no overlap) rather than overlapping, revised, or derived views of the same rows, and show each sheet's row count and time coverage range in that confirmation so an overlap is visible. The same rule applies to repeated `--input-file`.
52
68
  - **Multi-file type conflicts** — when the same column has different inferred types across files, present each conflict and resolve with `--type-resolutions` on `convert` (see [transform](transform.md)).
53
69
 
54
70
  ## Nested flattening (NDJSON/JSON records and JSON-encoded CSV/TSV/Excel cells)
@@ -48,7 +48,9 @@ ae-cli data-integration convert \
48
48
  --output-dir '.ae-cli/data-integration/runs/<run-id>'
49
49
  ```
50
50
 
51
- The command never modifies the source. Inspect `manifest.json`; summarize valid and quarantined counts and the block reason. If `manifest.output.skipped_fields` is present, tell the user how many `#ip`/`#uuid` values were invalid and dropped (the rows were otherwise kept); if `manifest.output.lan_ip_records` is present, tell the user that many `#ip` values are private/LAN addresses that AE cannot geolocate. Neither blocks the manifest. If `manifest.output.flatten_misses` is present — or a stderr `Warning: flatten rule "X" did not materialize for N row(s).` fires a `flatten_rules` path missed some rows: re-check the dot path against the source shape (or confirm the column is legitimately optional); the rows are otherwise kept. A stderr `Warning: … column count different from the header row …` means ragged CSV/TSV rows were tolerated (extra fields dropped, missing fields treated as empty) — surface it as a data-quality note. A blocked manifest whose reason is `The source contained no data rows.` means the file had zero data rows; do not re-run the same command on it. Do not expose rows from `invalid.rows.jsonl` unless the user specifically asks to inspect the local quarantine. For the full failure taxonomy and how to respond, see [error handling](error-handling.md).
51
+ Merging several sources into one run assumes they are mutually exclusive partitions of the same data set. Confirm that with the user before converting matching headers do not rule out a detail/summary pair or an original/revised pairand present each source's row count and time coverage range so an overlap is visible. The same applies to `--merge-sheets` (see [source-inspect.md](source-inspect.md)).
52
+
53
+ The command never modifies the source. Inspect `manifest.json`; summarize valid and quarantined counts and the block reason, and check the conservation equation first: `manifest.output.source_rows` must equal `valid_records + invalid_records` — a mismatch means rows were dropped or duplicated between the source and the output, so report the three numbers to the user and do not upload until it is explained. A salvage run's `source_rows` is only the rows it re-processed, not the whole file. If `manifest.output.skipped_fields` is present, tell the user how many `#ip`/`#uuid` values were invalid and dropped (the rows were otherwise kept); if `manifest.output.lan_ip_records` is present, tell the user that many `#ip` values are private/LAN addresses that AE cannot geolocate. Neither blocks the manifest. If `manifest.output.flatten_misses` is present — or a stderr `Warning: flatten rule "X" did not materialize for N row(s).` fires — a `flatten_rules` path missed some rows: re-check the dot path against the source shape (or confirm the column is legitimately optional); the rows are otherwise kept. A stderr `Warning: … column count different from the header row …` means ragged CSV/TSV rows were tolerated (extra fields dropped, missing fields treated as empty) — surface it as a data-quality note. A blocked manifest whose reason is `The source contained no data rows.` means the file had zero data rows; do not re-run the same command on it. Do not expose rows from `invalid.rows.jsonl` unless the user specifically asks to inspect the local quarantine. For the full failure taxonomy and how to respond, see [error handling](error-handling.md).
52
54
 
53
55
  ## Re-report only the failed rows (salvage loop)
54
56
 
@@ -22,6 +22,28 @@ Classification order:
22
22
 
23
23
  Aggregated metrics, pivot tables, cross-tabs, model outputs, free-form documents, and records without real identity/time should normally use local analysis.
24
24
 
25
+ ### Time coverage is not native granularity
26
+
27
+ A parseable time column establishes only when the rows are stamped, not what period each metric
28
+ covers. A daily report and a cumulative snapshot both look like one row per user per point in time,
29
+ so they satisfy every condition above and are then ingested as per-period events — inflating totals
30
+ in a way that stays invisible in ratios, because numerator and denominator scale together.
31
+
32
+ Native granularity must come from the user, a data dictionary, or a complete period structure in the
33
+ data itself. Do not infer it from the file name, the first/last date, the interval between rows, or
34
+ the row count; none of those is evidence.
35
+
36
+ When the data carries numeric columns and any of the following holds, ask the user to state whether
37
+ each row's value is the amount that occurred in that period or the total accumulated up to that
38
+ point, and do not proceed until they answer:
39
+
40
+ - Paired start/end time columns (`start_date`/`end_date`, `period_begin`/`period_end`).
41
+ - Values for one identity that never decrease over time.
42
+ - Column names carrying a to-date sense (`cumulative`, `total`, `ltv`, `累计`, `总`).
43
+
44
+ An unanswered question, a cumulative snapshot, or overlapping periods route to local analysis
45
+ instead.
46
+
25
47
  ## Route to local analysis
26
48
 
27
49
  Choose local analysis when:
@@ -29,6 +51,7 @@ Choose local analysis when:
29
51
  - The user wants insights, not project ingestion.
30
52
  - UE identity or time prerequisites are missing.
31
53
  - Each row is an aggregate rather than a user/event record.
54
+ - The rows are a cumulative snapshot, or their native granularity could not be established.
32
55
  - Conversion would invent semantics or discard important structure.
33
56
  - The user declines an uncertain mapping or destination.
34
57