@thinkingai/ae-cli 6.1.17 → 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 (126) hide show
  1. package/README.md +2 -0
  2. package/README.zh.md +6 -0
  3. package/dist/{auth-77BUFLGC.js → auth-QPEVVBKT.js} +20 -15
  4. package/dist/auth-UMJAA72O.js +15 -0
  5. package/dist/{capability-72DTW5M2.js → capability-ASI62I4A.js} +53 -15
  6. package/dist/{chunk-UW5UN47B.js → chunk-533JK3YX.js} +2 -0
  7. package/dist/{chunk-UOUS37JQ.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-DAVKYVMW.js → chunk-B5HMQAOM.js} +31 -540
  11. package/dist/{chunk-VR3LCBHW.js → chunk-DKEXH6Q5.js} +6 -5
  12. package/dist/{chunk-VTWMIC5L.js → chunk-E2JKXMVO.js} +2 -0
  13. package/dist/{chunk-LYVNONC4.js → chunk-FR2EDAQM.js} +36 -0
  14. package/dist/{chunk-ILIU36SU.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-GT46FPXN.js → chunk-KEWVSO64.js} +18 -8
  18. package/dist/chunk-KZ6ZHF5J.js +294 -0
  19. package/dist/chunk-O6K2ODUE.js +318 -0
  20. package/dist/chunk-QL6R6TSK.js +25215 -0
  21. package/dist/{chunk-4SGZG4XY.js → chunk-RWXIXOZG.js} +28 -13
  22. package/dist/{chunk-YA6SMTXG.js → chunk-S5PONFBM.js} +5 -4
  23. package/dist/{chunk-4NN5IWVN.js → chunk-T6OSFQZD.js} +2 -0
  24. package/dist/{chunk-PTE56QPL.js → chunk-TKHSULJT.js} +6 -0
  25. package/dist/{chunk-6EIJSNBD.js → chunk-Y74WTIKJ.js} +2 -0
  26. package/dist/{client-TKG4WBHN.js → client-N7NGZUNI.js} +6 -4
  27. package/dist/{community-report-client-FI4LNVYS.js → community-report-client-6OL6GBQW.js} +3 -3
  28. package/dist/{config-RE6CMGPK.js → config-XNMACCKX.js} +10 -8
  29. package/dist/{data-integration-XQYB4X4F.js → data-integration-4NZGZ45E.js} +2502 -272
  30. package/dist/index.js +137 -1245
  31. package/dist/{local-data-upload-client-BWHSUQQK.js → local-data-upload-client-EFHDLXFG.js} +3 -3
  32. package/dist/{memory-YK33G4T7.js → memory-HBJQ2GC5.js} +8 -7
  33. package/dist/{metadata-UILXHBWF.js → metadata-62EWBEIG.js} +12 -11
  34. package/dist/{model-NR3JHFSJ.js → model-BHLJ2QMT.js} +10 -9
  35. package/dist/personal-semantic-preference-JBVMAI7L.js +240 -0
  36. package/dist/project-semantic-BQ4MPUS5.js +1115 -0
  37. package/dist/sync-MUWPRM5I.js +522 -0
  38. package/dist/{te-agent-4BKBODMF.js → te-agent-WQZTMEK4.js} +12 -10
  39. package/dist/{te-analysis-O6DCO6BS.js → te-analysis-MNCG65LU.js} +534 -48
  40. package/dist/{te-community-HLC43QKH.js → te-community-BNE6KREL.js} +8 -7
  41. package/dist/{te-dataops-EJP56W3K.js → te-dataops-TXLKM2W6.js} +469 -68
  42. package/dist/{te-engage-FGBGQ4IY.js → te-engage-TPKLRVTL.js} +84 -10
  43. package/dist/{te-experiment-SO5MPDMJ.js → te-experiment-6BYKTCY7.js} +303 -12
  44. package/dist/{te-kb-SQCLHG6X.js → te-kb-BUD6N3NI.js} +312 -25
  45. package/dist/{te-system-YARIK4S5.js → te-system-44LZJDTQ.js} +573 -37
  46. package/dist/{te-team-EFKWYKMK.js → te-team-XMP47YXE.js} +10 -7
  47. package/dist/{update-OGPSZM5A.js → update-UODFOZ4P.js} +9 -7
  48. package/package.json +18 -9
  49. package/skills/ae-agent/SKILL.md +3 -4
  50. package/skills/ae-agent/references/edit-skill.md +3 -0
  51. package/skills/ae-agent/references/get-skill-content.md +1 -1
  52. package/skills/ae-agent/references/rescan-skills.md +15 -13
  53. package/skills/ae-agent/references/upload-skill.md +7 -4
  54. package/skills/ae-analysis/SKILL.md +45 -4
  55. package/skills/ae-analysis/metadata_resolution.md +38 -4
  56. package/skills/ae-analysis/references/analysis_data_retrieval.md +29 -0
  57. package/skills/ae-analysis/references/analysis_gateway_assets.md +1 -1
  58. package/skills/ae-analysis/references/asset_authentication_export.md +22 -0
  59. package/skills/ae-analysis/references/asset_authentication_list.md +18 -14
  60. package/skills/ae-analysis/references/asset_authentication_update.md +29 -14
  61. package/skills/ae-analysis/references/audience_models.md +18 -0
  62. package/skills/ae-analysis/references/command_index.md +19 -11
  63. package/skills/ae-analysis/references/dashboard_get.md +18 -1
  64. package/skills/ae-analysis/references/dashboard_update.md +3 -0
  65. package/skills/ae-analysis/references/personal_semantic_preference_add.md +23 -0
  66. package/skills/ae-analysis/references/personal_semantic_preference_delete.md +17 -0
  67. package/skills/ae-analysis/references/personal_semantic_preference_get.md +19 -0
  68. package/skills/ae-analysis/references/personal_semantic_preference_list.md +21 -0
  69. package/skills/ae-analysis/references/personal_semantic_preference_update.md +19 -0
  70. package/skills/ae-analysis/references/project_timezone_update.md +13 -4
  71. package/skills/ae-analysis/references/property_create.md +2 -0
  72. package/skills/ae-analysis/references/super_metadata_batch_create.md +2 -0
  73. package/skills/ae-analysis/references/user_cluster_models.md +2 -0
  74. package/skills/ae-analysis/references/user_cluster_update.md +8 -4
  75. package/skills/ae-analysis/references/user_tag_create.md +7 -1
  76. package/skills/ae-analysis/references/user_tag_models.md +8 -2
  77. package/skills/ae-data-integration/SKILL.md +24 -4
  78. package/skills/ae-data-integration/references/custom-layer.md +93 -0
  79. package/skills/ae-data-integration/references/error-handling.md +145 -0
  80. package/skills/ae-data-integration/references/handoff.md +77 -18
  81. package/skills/ae-data-integration/references/local-analysis.md +1 -1
  82. package/skills/ae-data-integration/references/reuse.md +9 -5
  83. package/skills/ae-data-integration/references/sink-upload.md +1 -1
  84. package/skills/ae-data-integration/references/source-inspect.md +35 -13
  85. package/skills/ae-data-integration/references/tracking-plan.md +7 -5
  86. package/skills/ae-data-integration/references/transform.md +12 -10
  87. package/skills/ae-data-integration/references/ue-mapping.md +33 -11
  88. package/skills/ae-data-integration/references/ue-routing.md +23 -0
  89. package/skills/ae-dataops/SKILL.md +11 -1
  90. package/skills/ae-dataops/references/dataops-backfill.md +135 -0
  91. package/skills/ae-engage/SKILL.md +5 -0
  92. package/skills/ae-engage/references/build-task-save-guide.md +14 -1
  93. package/skills/ae-engage/references/save-flow.md +37 -1
  94. package/skills/ae-engage/references/save-task.md +88 -0
  95. package/skills/ae-experiment/SKILL.md +14 -4
  96. package/skills/ae-experiment/references/manage_feature_whitelist.md +66 -0
  97. package/skills/ae-experiment/references/manage_guardrail_metrics.md +26 -0
  98. package/skills/ae-experiment/references/save_experiment.md +1 -1
  99. package/skills/ae-experiment/references/save_metric.md +20 -8
  100. package/skills/ae-kb/SKILL.md +121 -10
  101. package/skills/ae-project-semantic/SKILL.md +193 -0
  102. package/skills/ae-project-semantic/references/query-routing-v5.md +165 -0
  103. package/skills/ae-project-semantic/references/recommendation-quality.md +68 -0
  104. package/skills/ae-system/SKILL.md +28 -30
  105. package/skills/ae-system/references/channel-management.md +303 -0
  106. package/dist/auth-2WTQOP77.js +0 -14
  107. package/dist/capability-PJHNI4GJ.js +0 -315
  108. package/dist/chunk-4KVPKXFX.js +0 -98
  109. package/dist/chunk-C4MGVGJW.js +0 -13
  110. package/dist/chunk-P3FGXJTU.js +0 -349
  111. package/dist/chunk-QGM4M3NI.js +0 -37
  112. package/dist/chunk-RGKJGKT7.js +0 -71
  113. package/dist/chunk-SAU3QFIQ.js +0 -556
  114. package/dist/chunk-VPKZ7I72.js +0 -509
  115. package/dist/chunk-ZZUOD757.js +0 -598
  116. package/dist/memory-CHRU2F7W.js +0 -893
  117. package/dist/metadata-XXR34N5P.js +0 -339
  118. package/dist/model-K3KLWIW6.js +0 -139
  119. package/dist/sync-FCKOVWWS.js +0 -10261
  120. package/dist/te-agent-HLW4VTQK.js +0 -3893
  121. package/dist/te-analysis-ZMNGOVNW.js +0 -8958
  122. package/dist/te-community-6HPBWJUZ.js +0 -1859
  123. package/dist/te-dataops-HDRUXY4K.js +0 -2208
  124. package/dist/te-engage-RAK5PESW.js +0 -4898
  125. package/dist/te-experiment-VZF7BT6G.js +0 -770
  126. package/dist/te-system-Z77IKZFN.js +0 -2213
@@ -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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ae-data-integration
3
- description: "Bring local CSV, TSV, TXT, JSON, JSONL (NDJSON), XLS, and XLSX files into AE end-to-end: identify the source's business meaning, generate and confirm a tracking plan, transform rows into UE records, and upload. Also supports privacy-preserving local analysis and handing a small file to AE Agent. Use whenever a user wants to import offline/local data into AE or analyze a file without uploading it."
3
+ description: "Bring local CSV, TSV, TXT, JSON, JSONL (NDJSON), XLS, and XLSX files into AE end-to-end: identify the source's business meaning, generate and confirm a tracking plan, transform rows into UE records, and upload. Also supports privacy-preserving local analysis and handing a small file to AE Agent. Use whenever a user wants to import offline/local data into AE or analyze a file without uploading it. Trigger words: 本地数据导入 / 离线数据 / 数据文件 / 文件导入 / 文件上报 / CSV 导入 / Excel 导入 / TSV 导入 / JSON 导入 / 导入到 AE / 导入到 ThinkingData / local data import / import local file."
4
4
  ---
5
5
 
6
6
  # AE Data Integration
@@ -17,22 +17,42 @@ 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.
23
24
  - If a batch times out or loses the network, treat that batch as unknown. Stop. Ask the user to verify receiver/AE data before the user chooses `--resume-from`; never resume automatically.
24
25
  - Local analysis stays local. AE Agent attachment is a separate, confirmed branch with a 50 MB per-file limit.
25
26
 
27
+ ## When to use / When NOT to use
28
+
29
+ Use this skill when the user wants to bring a **local data file** (CSV/TSV/TXT/JSON/JSONL/XLS/XLSX) into AE, or analyze such a file locally without uploading.
30
+
31
+ | User intent | Use this instead |
32
+ | --- | --- |
33
+ | How to integrate the SDK / tracking code / LogBus2 config / reporting-error triage (usage Q&A, no local file) | ae-data-integration-helper |
34
+ | Database / datasource direct sync (MySQL, DataX, data warehouse, data-dev platform) | ae-dataops |
35
+ | Community content (posts / comments / chat / WeCom groups) insight or submission | ae-community |
36
+ | Generate / upload a project-level tracking plan (source material is PRD / chat / template / code; deliverable is a real platform tracking plan) | ae-generate-tracking-plan |
37
+ | Upload documents / URLs to a knowledge base | ae-kb |
38
+ | Reports / dashboards / queries / governance on data already in AE | ae-analysis |
39
+
40
+ This skill also produces a tracking-plan draft (`source_type: data`) as a governance prerequisite; that draft is an input to ae-generate-tracking-plan, not a substitute for its five-phase platform plan.
41
+
26
42
  ## Workflow
27
43
 
28
44
  Walk the four submodules in order. Each submodule is its own reference; follow it and come back here for the next step.
29
45
 
30
46
  1. **Source — business identification.** Read [references/source-inspect.md](references/source-inspect.md). Profile every file fully, infer its business meaning using business-doc / user-prompt priors, then pick a branch via [references/ue-routing.md](references/ue-routing.md).
31
- 2. **Reuse check.** If a `.ae-data-integration/index.json` exists and the profile is `ue_eligible`, read [references/reuse.md](references/reuse.md) and match the recommended mapping against the handoff index. A match proposes a frozen package; after one explicit confirmation, run the returned `transform.mjs` command and jump to Sink (step 5). No match → continue.
32
- 3. **Tracking plan.** Read [references/tracking-plan.md](references/tracking-plan.md). Generate the event/property plan from the profile and get a single explicit confirmation from the user before touching data. The plan is a separate, required deliverable from the transform mapping: a user who supplies a column→field mapping directly has **not** completed this step, so build the plan from the confirmed mapping anyway. `user_set` still requires a plan (no events; every property becomes a user property). This step runs for **every** file: a second or later file merges its new events and properties into the existing project plan (tracking-plan.md step 4) — an existing plan is never a reason to skip it.
47
+ 2. **Reuse check.** If the profile is `ue_eligible`, read [references/reuse.md](references/reuse.md) and match the recommended mapping against the handoff index. `reuse` searches the current directory's `.ae-cli/data-integration/` upward, then `~/.ae-cli/data-integration/`, so a package written elsewhere is still found. A match proposes a frozen package; after one explicit confirmation, run the returned `transform.mjs` command and jump to Sink (step 5). No match → continue.
48
+ 3. **Tracking plan.** Read [references/tracking-plan.md](references/tracking-plan.md). The plan is generated from the mapping (`plan --mapping`), so confirm the recommended mapping's key system fields with the user first — `mode`, `#account_id`/`#distinct_id`, `#time` + timezone, `#event_name`, `#ip`/`#uuid` (see [references/transform.md](references/transform.md) steps 1–5) — then generate the event/property plan and get a single explicit confirmation from the user before touching data. The plan is a separate, required deliverable from the transform mapping: a user who supplies a column→field mapping directly has **not** completed this step, so build the plan from the confirmed mapping anyway. `user_set` still requires a plan (no events; every property becomes a user property). This step runs for **every** file: a second or later file merges its new events and properties into the existing project plan (tracking-plan.md step 4) — an existing plan is never a reason to skip it.
33
49
  4. **Transform.** Read [references/transform.md](references/transform.md). Map columns to AE system fields and properties, convert, and quarantine dirty rows per [references/ue-mapping.md](references/ue-mapping.md).
34
50
  5. **Sink — upload.** Read [references/sink-upload.md](references/sink-upload.md). Resolve the destination, dry-run, confirm, then upload per [references/sync-json-upload.md](references/sync-json-upload.md). `receiver_accepted` is not persistence: after a ~1-minute ingestion delay, verify the data landed with ae-cli (`tracking live-data list` / `tracking ingest summary` / `tracking ingest-error list`) rather than telling the user to check the console.
35
- 6. **Handoff.** Read [references/handoff.md](references/handoff.md). Export the reusable package (frozen mapping + transform script + plan reference) so the next same-shape file skips the full pipeline.
51
+ 6. **Handoff.** Read [references/handoff.md](references/handoff.md). Export the reusable package (pipeline descriptor + frozen mappings + stage executors + docs) and a shareable zip; in the completion response, state the absolute zip path, the package directory, and the one-line way to run the next same-shape file.
52
+
53
+ ## Error handling
54
+
55
+ When a step fails, classify the failure before acting — see [references/error-handling.md](references/error-handling.md). A quarantined row, a ragged line, and a disk-full are three different problems with three different responses: match on the error `code`, never retry a parse failure by guessing the encoding, and never report a program failure as a data problem.
36
56
 
37
57
  ## Local analysis branch
38
58
 
@@ -0,0 +1,93 @@
1
+ # Project custom layer (verify + salvage overlays)
2
+
3
+ The handoff package is deliberately **standard flow only**: it ships the generic
4
+ stage executors and never embeds environment-specific workarounds such as SQL
5
+ direct queries or multi-round salvage loops. Some projects need those — a hard
6
+ per-event persistence judge, or a repeated salvage loop for a dirty source. Add
7
+ them as a **custom layer next to the package**, never by editing the generic
8
+ package itself.
9
+
10
+ ## When to add a layer
11
+
12
+ - **Hard persistence judge.** `bin/verify.py` is a soft check: it computes the
13
+ submit window and expected counts from the local UE output, then shows the
14
+ `tracking ingest summary` payload before and after for comparison. It does not
15
+ parse that payload into per-event counts — the capability's `data` shape is
16
+ server-defined, and a shared project's window delta cannot be attributed to one
17
+ import. When the project requires an automated per-event ✓/✗ verdict, overlay a
18
+ SQL layer that reads the project's event/user tables directly.
19
+ - **Multi-round salvage.** `bin/run.sh` already prints a single-round salvage
20
+ hint when `invalid.rows.jsonl` is non-empty. For sources that fail in layers,
21
+ wrap that command in a loop that re-feeds each round's quarantine into the next
22
+ `--salvage-from` until nothing fails or the user stops.
23
+
24
+ ## Rules
25
+
26
+ 1. Put overlays in a sibling directory (e.g. `custom/`) under the package root,
27
+ and reference `../pipeline.json` / `../index.json` — never modify the frozen
28
+ mappings, `bin/`, or the generated docs.
29
+ 2. Keep every existing confirmation gate. A custom layer may add gates; it must
30
+ not remove the `--confirm` upload gate or the plan/shape gates.
31
+ 3. Keep secrets in `.local/target.env`. Never write APPID, endpoints, tokens, or
32
+ raw data values into an overlay file.
33
+ 4. An overlay is project-specific. Do not copy it into another project's package
34
+ without re-confirming the environment assumptions (table names, APPID, host).
35
+
36
+ ## Skeleton 1 — SQL verify (hard per-event judge)
37
+
38
+ The reference deployment queries the ingested tables directly. **Table and
39
+ column names vary by deployment** (`v_event_1` / `v_user_1` and `$part_date` /
40
+ `$part_event` are examples only) — confirm them against the project's receiver
41
+ schema before use, and keep the query read-only.
42
+
43
+ ```bash
44
+ # custom/verify.sh — hard judge; requires the SQL capability to be available.
45
+ # usage: custom/verify.sh <run-dir> [--baseline|--check]
46
+ # Reuses bin/verify.py's window/count computation idea, but answers the question
47
+ # "did exactly these events land?" with a direct count over the event table.
48
+ ```
49
+
50
+ The query shape (adapt table/column names):
51
+
52
+ ```text
53
+ select <event_column>, count(*)
54
+ from <event_table>
55
+ where <date_partition_column> between '<window-start>' and '<window-end>'
56
+ group by 1
57
+ ```
58
+
59
+ Compare the per-event delta (after upload minus before upload) against the
60
+ expected counts from `valid.ue.jsonl`. User-profile rows are overwrite-writes,
61
+ so a zero user-table delta is normal; compare event rows per event, not totals.
62
+
63
+ Keep this layer read-only and non-blocking: report the verdict, never retransmit
64
+ automatically on a mismatch — first check `tracking ingest-error list` for the
65
+ silent-drop reason.
66
+
67
+ ## Skeleton 2 — multi-round salvage loop
68
+
69
+ `bin/run.sh` prints the one-round hint. Wrap it into a loop that re-processes each
70
+ round's quarantine against the fixed mapping until clean or the user stops:
71
+
72
+ ```bash
73
+ # custom/salvage.sh <run-dir> — re-process quarantined rows round by round.
74
+ # Each round's valid.ue.jsonl is disjoint from earlier rounds', so upload each
75
+ # round independently (same --confirm and --allow-clean-subset gates).
76
+ round=1
77
+ while [ -s "<run-dir>/invalid.rows.jsonl" ]; do
78
+ ae-cli data-integration convert \
79
+ --input-file '<same-source>' \
80
+ --mapping '<fixed-mapping.json>' \
81
+ --salvage-from "<run-dir>/invalid.rows.jsonl" \
82
+ --output-dir "<run-dir>-salvage-$round"
83
+ round=$((round + 1))
84
+ # stop condition is the user's: a row that still fails after a fix is a real
85
+ # data defect, not a code bug.
86
+ done
87
+ ```
88
+
89
+ See [transform.md](transform.md) for the `--salvage-from` semantics (single-file
90
+ only; the source must be the same file that produced the quarantine) and
91
+ [handoff.md](handoff.md) for the package the overlay attaches to. Persistence
92
+ verification always follows [sink-upload.md](sink-upload.md): `receiver_accepted`
93
+ is not durability.
@@ -0,0 +1,145 @@
1
+ # Error handling
2
+
3
+ Failures in the data-integration pipeline split into three classes. Each class has a distinct
4
+ response; an agent that mixes them up wastes retries and misleads the user.
5
+
6
+ | Class | What it is | Response |
7
+ | --- | --- | --- |
8
+ | Abnormal data | Rows or fields that do not meet UE rules | Quarantine the row or skip the field, report counts, salvage |
9
+ | File parsing exception | The source cannot be read as its detected format | Surface the parse error, do not retry by guessing; fix the file or format |
10
+ | Program execution exception | The program itself failed (disk, permissions, output dir, mapping mismatch) | Surface the exact error, fix the environment/inputs, re-run |
11
+
12
+ Every error leaves the CLI as the standard envelope `{ ok, data, error: { type, code, message, hint } }`
13
+ (JSON on stdout; progress and warnings on stderr). The `code` is the stable identifier an agent
14
+ branches on; `hint` is human-readable guidance.
15
+
16
+ ## Class 1 — Abnormal data
17
+
18
+ Happens inside `convert`. Two severities:
19
+
20
+ - **Whole-row quarantine.** Any error on a row — identity, time, event name, record type, or a
21
+ single property (type coercion, size/limit) — drops the entire row. The row is written to
22
+ `invalid.rows.jsonl` with its error codes and counted in `manifest.output.invalid_records`; the
23
+ manifest becomes `blocked`. Row-level codes: `MISSING_USER_ID`, `USER_ID_TOO_LONG`,
24
+ `INVALID_RECORD_TYPE`, `INVALID_TIME`, `TIME_OUT_OF_RANGE`, `INVALID_EVENT_NAME`,
25
+ `INVALID_ZONE_OFFSET`, `PROPERTY_TYPE_CONFLICT`, `PROPERTY_LIMIT_EXCEEDED`.
26
+ - **Field skip.** A `#ip` or `#uuid` value that violates its spec drops only that field and keeps
27
+ the row. Counts land in `manifest.output.skipped_fields`; private/LAN IPs are kept but counted in
28
+ `manifest.output.lan_ip_records`. Field-skip codes: `INVALID_IP`, `INVALID_UUID`. Never quarantined.
29
+
30
+ Responses:
31
+
32
+ - A blocked manifest is not a failure to retry blindly. Fix the mapping, then re-run `convert
33
+ --salvage-from <invalid.rows.jsonl>` to re-process only the quarantined rows (never re-send the
34
+ rows that already passed). Repeat the salvage loop until no rows fail or the user stops.
35
+ - An empty source (zero data rows) blocks with the reason `The source contained no data rows.` —
36
+ a distinct, clearer message than a failed validation run. Do not re-run the same command on the
37
+ same empty file expecting a different result.
38
+ - Ragged delimited rows (a CSV/TSV row whose column count differs from the header) are tolerated:
39
+ extra fields are dropped and missing fields are treated as empty, and a stderr warning reports
40
+ how many rows were ragged. A row that becomes missing its identity/time because a field was
41
+ absent is then quarantined normally. Treat the warning as a data-quality signal for the user.
42
+
43
+ ## Class 2 — File parsing exceptions
44
+
45
+ The source cannot be parsed as its detected format. The agent's job is to report precisely and let
46
+ the user decide — never to guess an encoding or structure and retry silently.
47
+
48
+ | Code | Meaning | What to do |
49
+ | --- | --- | --- |
50
+ | `LOCAL_DATA_INPUT_NOT_FOUND` | Path is not a readable file | Ask for the correct path |
51
+ | `LOCAL_DATA_FILE_TOO_LARGE` | XLS over 1 GB | Convert to XLSX or split the workbook |
52
+ | `LOCAL_DATA_INPUT_INVALID` | Generic parse failure (malformed CSV/TSV/JSON/XLS) | Verify encoding and structure, then retry without changing the source |
53
+ | `LOCAL_DATA_JSONL_INVALID` | A JSONL line is not valid JSON (`location.record` names the line) | Point at the offending record |
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 (sheet recognition accepts `<sheet>` and namespaced `<x:sheet>` alike, so this is a real metadata gap) |
56
+ | `LOCAL_DATA_SET_NOT_FOUND` / `LOCAL_DATA_SET_REQUIRED` | Sheet or JSON Path not found / ambiguous | Ask which `--data-set` to use |
57
+
58
+ A parse error is never a reason to change the mapping or the tracking plan. Report the code and
59
+ hint verbatim, and ask the user to fix the file (re-export, re-encode, or split).
60
+
61
+ ## Class 3 — Program execution exceptions
62
+
63
+ The program itself failed; the source data is usually fine. These must never be mislabeled as
64
+ parse errors: a per-row callback failure (for example a disk that filled up mid-convert) propagates
65
+ as itself, not as `LOCAL_DATA_INPUT_INVALID`.
66
+
67
+ | Code | Meaning | What to do |
68
+ | --- | --- | --- |
69
+ | `LOCAL_DATA_OUTPUT_NOT_EMPTY` | The output directory must be new or empty | Point convert at a fresh `<run-id>` directory |
70
+ | `LOCAL_DATA_SOURCE_CHANGED` / `LOCAL_DATA_SOURCE_FORMAT_CHANGED` | The source no longer matches the mapping fingerprint/format | Re-run inspect and review a new mapping |
71
+ | `LOCAL_DATA_MAPPING_INVALID` / `LOCAL_DATA_MAPPING_INVALID_JSON` / `LOCAL_DATA_MAPPING_NOT_FOUND` | The mapping cannot be read or validated | Re-read the mapping reference, fix the mapping file |
72
+ | `LOCAL_DATA_SALVAGE_INVALID` / `LOCAL_DATA_SALVAGE_EMPTY` / `LOCAL_DATA_SALVAGE_NO_MATCH` | The salvage file is not a valid quarantine file, is empty, or lists no rows from this source | Point at the correct `invalid.rows.jsonl` from the same source |
73
+ | `LOCAL_DATA_TYPE_CONFLICTS_UNRESOLVED` / `LOCAL_DATA_TYPE_RESOLUTIONS_INVALID` | Cross-file column type conflicts need explicit resolutions | Build `--type-resolutions` |
74
+ | `LOCAL_DATA_PLAN_INVALID_EVENT_NAME` / `LOCAL_DATA_PLAN_EVENT_NAMES_REQUIRED` / `LOCAL_DATA_PLAN_INVALID_LANG` | Tracking-plan draft inputs are invalid | Fix the event name(s) or `--lang` |
75
+ | `LOCAL_DATA_HANDOFF_INDEX_INVALID` / `LOCAL_DATA_HANDOFF_PLAN_NOT_FOUND` / `LOCAL_DATA_HANDOFF_PLAN_INVALID` | Handoff index/plan cannot be read | Point at a valid handoff directory/plan file |
76
+ | `LOCAL_DATA_ENDPOINT_INVALID` / `LOCAL_DATA_APPID_INVALID` / `LOCAL_DATA_BATCH_SIZE_INVALID` / `LOCAL_DATA_COMPRESS_INVALID` / `LOCAL_DATA_RESUME_INVALID` / `LOCAL_DATA_RESUME_OUT_OF_RANGE` | Upload arguments are invalid | Fix the flag before uploading |
77
+ | `LOCAL_DATA_MANIFEST_INVALID` / `LOCAL_DATA_UE_FILE_INVALID` / `LOCAL_DATA_UE_FILE_NOT_FOUND` / `LOCAL_DATA_UE_FILE_CHANGED` / `LOCAL_DATA_UE_COUNT_MISMATCH` / `LOCAL_DATA_MANIFEST_FILE_MISMATCH` | Upload preconditions fail | Re-check the manifest and UE file pairing |
78
+ | `LOCAL_DATA_CLEAN_SUBSET_CONFIRMATION_REQUIRED` | Uploading from a blocked manifest needs a separate clean-subset decision | Confirm the subset explicitly before `--allow-clean-subset` |
79
+
80
+ Write failures (disk full `ENOSPC`, permission denied `EACCES`) do not hang the command: the
81
+ output streams fail with a clear `Failed to write "<path>"` message telling the user to check disk
82
+ space and directory permissions. Fix the environment, then re-run into a fresh output directory.
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
+
137
+ ## Cross-cutting rules
138
+
139
+ - Classify first, act second. Match on `code`, not on message text.
140
+ - A parse error is not a data problem; a program error is not a parse error. Do not conflate them
141
+ when reporting back to the user.
142
+ - Never retry a parse failure by guessing the encoding, delimiter, or header layout. Show the
143
+ error and ask.
144
+ - After any fix, re-run from the start of the step that failed; do not resume a half-written run
145
+ or reuse a partially populated output directory.
@@ -1,43 +1,102 @@
1
1
  # Handoff (reusable package)
2
2
 
3
- After a successful run, export a **handoff package** so the next file of the same shape skips the full pipeline. The package freezes the transform logic (the confirmed mapping), the runnable transform script, and the tracking-plan reference. It does **not** re-freeze the raw data or any upload secrets.
3
+ After a successful run, export a **handoff package** so the next file of the same shape skips the full pipeline. The package is a DataX-style pipeline — a declarative `source → transform → sink` descriptor plus generic stage executors that dispatch to `ae-cli data-integration` subcommands. It freezes the transform logic (the confirmed mappings) and the tracking-plan reference, and ships a `bin/` directory that a human or agent can run directly. It never re-freezes raw data or upload secrets.
4
4
 
5
5
  ## When
6
6
 
7
- Run handoff after Transform — or after Sink — once the mapping is confirmed. It is local-only and idempotent: re-running it for the same table refreshes the package in place.
7
+ Run handoff after Transform — or after Sink — once the mappings are confirmed. It is local-only and idempotent: re-running it for the same table refreshes the package in place.
8
8
 
9
9
  ## CLI
10
10
 
11
11
  ```
12
- ae-cli data-integration handoff --mapping <mapping> [--plan-file <draft.json>] [--out-dir .ae-data-integration]
12
+ ae-cli data-integration handoff --mapping <mapping> [--mapping <mapping2> ...] [--plan-file <draft.json>] [--pushurl <url>] [--project-id <id>] [--out-dir .ae-cli/data-integration]
13
13
  ```
14
14
 
15
- - `--mapping` — the confirmed `ae-local-data-mapping/v1` mapping (the frozen transform logic, including `value_mapping` and `flatten_rules`).
16
- - `--plan-file` — optional tracking-plan `draft.json` to reference inside the package (`plan.json`).
17
- - `--out-dir` — handoff root. Default `.ae-data-integration/` (travels with the project).
18
- - `--dry-run` previews the fingerprint, files, and index path without writing.
15
+ - `--mapping` — one or more confirmed `ae-data-integration-mapping/v1` mappings (the frozen transform logic, including `value_mapping` and `flatten_rules`). Repeat it for multi-sheet workbooks: one mapping per sheet.
16
+ - `--plan-file` — optional tracking-plan `draft.json` to reference inside each mapping directory (`plan.json`).
17
+ - `--pushurl` — optional receiver base URL to record as the reuse upload target (the sink endpoint is `pushurl` + `/sync_json`). Record it when the next same-shape file will most likely land at the same receiver.
18
+ - `--project-id` optional numeric destination project ID to record; `bin/upload.sh` derives the APPID from it via `project info get` at upload time.
19
+ - `--out-dir` — handoff root. Default `.ae-cli/data-integration/` (project workspace; travels with the project).
20
+ - `--dry-run` previews the fingerprints, target, file list, and zip path without writing.
19
21
 
20
22
  ## Package layout
21
23
 
22
24
  ```
23
- .ae-data-integration/
24
- index.json shared fingerprint index (reuse detection)
25
- <fingerprint[:16]>/
26
- mapping.json frozen mapping (re-runnable by convert)
27
- transform.mjs node transform.mjs <new-file> [<output-dir>]
28
- plan.json optional tracking-plan reference
25
+ .ae-cli/data-integration/ ← handoff root (= out-dir, project workspace)
26
+ pipeline.json source transform → sink descriptor (ae-data-integration-pipeline/v1)
27
+ index.json ← shared structure-fingerprint index (reuse detection)
28
+ shape.json per-mapping column baseline (shape gate)
29
+ <fingerprint[:16]>/ one directory per mapping
30
+ mapping.json frozen mapping
31
+ transform.mjs ← node transform.mjs <new-file> [<output-dir>]
32
+ plan.json ← optional tracking-plan reference
33
+ bin/ ← generic stage executors (read pipeline.json)
34
+ run.sh ← source → transform → plan (never uploads)
35
+ upload.sh ← sink (dry-run by default; --confirm uploads; resolves recorded target)
36
+ bind_mapping.py ← shape check + rebind sha256/data_set to the new file
37
+ summarize.py ← valid/quarantined counts
38
+ plan_check.py ← tracking-plan coverage gate (exit 3 on new events/properties)
39
+ verify.py ← soft persistence check (submit window vs ingest summary)
40
+ resolve_appid.py ← APPID derivation helper (project info get)
41
+ README.md / RUNBOOK.md ← how to run, the four gates, persistence verification
42
+ .local/target.env.example ← destination template (no real secrets)
43
+ .gitignore ← inbox/ runs/ .local/target.env
44
+ inbox/ runs/ ← daily input / per-run outputs
29
45
  ```
30
46
 
31
- The `transform.mjs` wrapper shells out to `ae-cli data-integration convert`; it takes the new file path as its first argument, never bakes the source path, and re-stamps `source.sha256` with the new file's fingerprint before converting. Reuse it only for a file of the **same shape** (same header/schema and format) the transform logic is frozen, but the content guard is re-bound to each specific file.
47
+ A shareable archive is written next to the package root: `<parent>/ae-data-integration-handoff-<fingerprint[:8]>.zip`. The zip carries only this handoff round the frozen mappings just written, a scoped `index.json` (just this round's entries), and the generic executors/docs not the accumulated history, which stays in `.ae-cli/data-integration/` for reuse matching.
48
+
49
+ ## Pipeline descriptor
50
+
51
+ `pipeline.json` declares the three stages and their types. Only `source: local_file` and `sink: restful_sync_json` are implemented today; the `type` fields reserve logbus / datax / mysql for later phases. `bin/run.sh` and `bin/upload.sh` read the descriptor and dispatch each stage by its `type` to `ae-cli data-integration inspect / convert / upload` — they are executors, not a second runtime engine.
52
+
53
+ ## Recorded destination
54
+
55
+ `pipeline.json` → `sink.params` records `pushurl` and `project_id` when the handoff
56
+ was run with those flags. Reuse defaults to that target, but **`bin/upload.sh`
57
+ never sends without `--confirm`**, so the operator re-confirms the address and
58
+ project on every reuse. Resolution order at upload time:
59
+
60
+ - endpoint: recorded `pushurl` (+ `/sync_json`), else `AE_ENDPOINT`.
61
+ - APPID: `AE_APPID`, else derived via `ae-cli project info get --project-id <id>`
62
+ (see `bin/resolve_appid.py`; set `AE_APPID` when that payload lacks `appid`).
63
+ - project id: recorded `project_id`, else `AE_PROJECT_ID`.
64
+
65
+ `project info get` returns `data.appid` at the top level (verified against the AE
66
+ demo host); `bin/resolve_appid.py` reads that exact field and prints it, falling
67
+ back to `AE_APPID` when the field is absent or not a non-empty string.
68
+
69
+ ## Project custom layers
70
+
71
+ For projects that need a hard per-event SQL judge or a multi-round salvage loop
72
+ (the reference package's environment-specific workarounds), overlay a custom layer
73
+ next to the package instead of editing it — see [custom-layer.md](custom-layer.md).
74
+
75
+ ## Four confirmation gates
76
+
77
+ The RUNBOOK and the scripts enforce four gates. The first two run automatically; the last two always need human confirmation:
78
+
79
+ 1. **Shape gate** — `bind_mapping.py` compares the new file's column set against `shape.json` and fails fast on a mismatch. A changed shape means the frozen logic was never reviewed for it: re-run the full pipeline.
80
+ 2. **Transform** — `ae-cli data-integration convert` per mapping; quarantined rows go to `invalid.rows.jsonl`, never silently dropped.
81
+ 3. **Tracking-plan gate** — `plan_check.py` verifies every produced event/property already exists in `plan.json`; new ones exit 3 and must be merged into the project plan first.
82
+ 4. **Sink gate** — `upload.sh` is dry-run by default; `--confirm` is the explicit upload decision.
32
83
 
33
84
  ## Structure fingerprint and index
34
85
 
35
- Every handoff records one entry in `index.json` (`ae-data-integration-index/v1`) keyed by a **structure fingerprint** — a SHA-256 over the table shape: columns (source name + type), event model (`mode`, `event_name_field`, `record_type_field`), identity fields, and excluded columns. Business logic (`value_mapping`, transforms, `time_format`, and the fixed `default_event_name`) is excluded, so re-handing off the same table with new business rules refreshes the existing entry instead of forking a new one.
86
+ Every handoff records one entry per mapping in `index.json` (`ae-data-integration-index/v1`) keyed by a **structure fingerprint** — a SHA-256 over the table shape: the raw source columns (by name, reconstructed so flatten, exclude, and account-vs-distinct decisions don't move it), the format, and the event model (`mode`). Business logic (`value_mapping`, transforms, `time_format`, `flatten_rules`, `exclude_columns`, the fixed `default_event_name`, and the system-field assignments) is excluded, so re-handing off the same table with new business rules refreshes the existing entry instead of forking a new one.
87
+
88
+ Reuse matching is its own step — see [references/reuse.md](reuse.md).
36
89
 
37
- Reuse matching is its own step — see [references/reuse.md](reuse.md). It compares a new file's profile structure against this index and proposes the matching package, which the user confirms before skipping the full pipeline.
90
+ ## Completion response
91
+
92
+ After handoff succeeds, state the **absolute zip path**, the package directory, and the one-line way to run the next same-shape file:
93
+
94
+ ```
95
+ cd <out-dir> && bin/run.sh <new-file> # then bin/upload.sh runs/<run-id> --confirm
96
+ ```
38
97
 
39
98
  ## Safety rules
40
99
 
41
- - Treat the mapping, plan, generated artifacts, and the `.ae-data-integration/` directory as sensitive.
42
- - Never write APPID, endpoints, tokens, or raw data values into a handoff package. The package references the plan and the mapping; uploads still require an explicit, confirmed `upload` call.
100
+ - Treat the mappings, plans, generated artifacts, the `.ae-cli/data-integration/` directory, and the zip as sensitive.
101
+ - Never write APPID, tokens, or raw data values into a handoff package. The package records at most a destination `pushurl` and `project_id`; uploads still require an explicit, confirmed `upload` call, so the operator re-confirms the address and project each time.
43
102
  - Do not invent a mapping or plan. Handoff only packages what the user already confirmed.
@@ -1,6 +1,6 @@
1
1
  # Local analysis
2
2
 
3
- Keep the source on the local machine. Generated scripts and reports belong under `.ae-cli/data-integration/<run-id>/` with restrictive permissions.
3
+ Keep the source on the local machine. Generated scripts and reports belong under `.ae-cli/data-integration/runs/<run-id>/` with restrictive permissions.
4
4
  Set the directory to `0700` and generated scripts/reports to `0600`.
5
5
 
6
6
  ## Default report when no question is supplied
@@ -4,21 +4,25 @@ Before walking the full pipeline for a new file, check whether a **handoff packa
4
4
 
5
5
  ## When
6
6
 
7
- Run reuse right after `inspect`, only when the profile is `ue_eligible` and a `.ae-data-integration/index.json` exists. It is read-only and makes no writes.
7
+ Run reuse right after `inspect`, only when the profile is `ue_eligible` and a `.ae-cli/data-integration/index.json` exists. It is read-only and makes no writes.
8
8
 
9
9
  ## CLI
10
10
 
11
11
  ```
12
- ae-cli data-integration reuse --mapping <recommended_mapping> [--out-dir .ae-data-integration]
12
+ ae-cli data-integration reuse --mapping <recommended_mapping> [--out-dir .ae-cli/data-integration]
13
13
  ```
14
14
 
15
15
  - `--mapping` — the candidate mapping, typically `inspect`'s `recommended_mapping` (the new file's structure, inferred exactly as Source would infer it).
16
- - `--out-dir` — handoff root. Default `.ae-data-integration/`.
16
+ - `--out-dir` — handoff root. Optional. Without it, `reuse` searches in order: the current directory's `.ae-cli/data-integration/`, then each parent directory upward, then `~/.ae-cli/data-integration/` as a global fallback — so a package written in another directory or another session is still reachable.
17
17
  - `--dry-run` previews the fingerprint and match verdict without reading frozen packages.
18
18
 
19
+ ## Search paths
20
+
21
+ When `--out-dir` is omitted, the result carries `searched_paths` — the exact `index.json` paths probed, in order. A `matched: false` result with a non-empty `searched_paths` means all of them were checked; a missing global fallback (no `$HOME`) simply omits it from the list.
22
+
19
23
  ## Matching
20
24
 
21
- The command computes the same **structure fingerprint** the handoff index is keyed on — columns (source name + type), event model, identity fields, and excluded columns — and looks it up in `index.json`. Business logic (the frozen event name, `value_mapping`, transforms, `time_format`) is not part of the key, so a same-shape file with different content or a different file name still matches.
25
+ The command computes the same **structure fingerprint** the handoff index is keyed on — the raw source columns (by name, reconstructed so flatten, exclude, and account-vs-distinct decisions don't move it), the format, and the event model — and looks it up in `index.json`. Business logic (the frozen event name, `value_mapping`, transforms, `time_format`, `flatten_rules`, `exclude_columns`, and the system-field assignments) is not part of the key, so a same-shape file with different content or a different file name still matches.
22
26
 
23
27
  ## Result
24
28
 
@@ -26,7 +30,7 @@ The command computes the same **structure fingerprint** the handoff index is key
26
30
  - **Match** (`matched: true`): the result carries the matched package — `mapping_file`, optional `plan_file`, the frozen `default_event_name` (so the user sees which event name will be reused), and a `run` command:
27
31
 
28
32
  ```
29
- node .ae-data-integration/<fingerprint[:16]>/transform.mjs <new-input-file> [<output-dir>]
33
+ node .ae-cli/data-integration/<fingerprint[:16]>/transform.mjs <new-input-file> [<output-dir>]
30
34
  ```
31
35
 
32
36
  ## Confirmation gate
@@ -31,7 +31,7 @@ ae-cli data-integration upload \
31
31
  --dry-run
32
32
  ```
33
33
 
34
- Show the masked target, project, file fingerprint, record count, quarantined count, batch count, and persistence limitation. Re-state the system-field mapping first (`#type`/mode, `#account_id`, `#distinct_id`, `#time` + source timezone, `#event_name`), then re-list the final property mapping for every file or sheet being uploaded — source column → target AE name → type, grouped into event properties (`track`) and user properties (profile modes) — never a counts-only summary. Wait for explicit confirmation. Execute the same command without `--dry-run` only after confirmation. For a blocked manifest, first show the quarantine statistics and separately ask whether the user accepts uploading only valid rows; add `--allow-clean-subset` only after a clear yes.
34
+ Show the masked target, project, file fingerprint, record count, quarantined count, batch count, and persistence limitation. Re-state the system-field mapping first (`#type`/mode, `#account_id`, `#distinct_id`, `#time` + source timezone, `#event_name`), then re-list the final property mapping for every file or sheet being uploaded — source column → target AE name → type (+ `display_name`/`desc` when set), grouped into event properties (`track`) and user properties (profile modes), with each event's attached properties listed — never a counts-only summary. For a multi-sheet workbook, group by sheet. Wait for explicit confirmation. Execute the same command without `--dry-run` only after confirmation. For a blocked manifest, first show the quarantine statistics and separately ask whether the user accepts uploading only valid rows; add `--allow-clean-subset` only after a clear yes.
35
35
 
36
36
  `status=receiver_accepted` means receiver acceptance only, not durable storage. Say that persistence remains unverified. Never report success on this status alone.
37
37