@1aboveio/skills 1.0.1 → 1.0.3

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 (91) hide show
  1. package/README.md +25 -8
  2. package/bin/select-core.mjs +182 -50
  3. package/extensions/hide-copies.ts +21 -0
  4. package/package.json +116 -3
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/android-foundation/SKILL.md +1 -1
  7. package/skills/app-dev/app-debug/SKILL.md +1 -1
  8. package/skills/app-dev/better-auth/SKILL.md +1 -1
  9. package/skills/app-dev/data-catalog/SKILL.md +2 -1
  10. package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +2 -1
  11. package/skills/app-dev/data-catalog/references/field-definitions.md +2 -1
  12. package/skills/app-dev/data-catalog/references/format.md +6 -2
  13. package/skills/app-dev/data-catalog/scripts/data_catalog.py +15 -3
  14. package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +3 -2
  15. package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +15 -0
  16. package/skills/app-dev/data-quality/LICENSE +3 -0
  17. package/skills/app-dev/data-quality/SKILL.md +96 -0
  18. package/skills/app-dev/data-quality/agents/openai.yaml +3 -0
  19. package/skills/app-dev/data-quality/references/checks.md +56 -0
  20. package/skills/app-dev/data-quality/release-check.mjs +93 -0
  21. package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +12 -0
  22. package/skills/app-dev/data-quality/scripts/data_quality.py +405 -0
  23. package/skills/app-dev/data-quality/scripts/test_data_quality.py +277 -0
  24. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  25. package/skills/app-dev/monorepo/SKILL.md +1 -1
  26. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  27. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  28. package/skills/app-dev/pyspark/SKILL.md +1 -1
  29. package/skills/app-dev/python-backend/SKILL.md +1 -1
  30. package/skills/app-dev/shadcn/SKILL.md +1 -1
  31. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  32. package/skills/cicd/cloud-build/SKILL.md +1 -1
  33. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  34. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  35. package/skills/cicd/docker/SKILL.md +1 -1
  36. package/skills/cicd/github-actions/SKILL.md +1 -1
  37. package/skills/cicd/github-flow/SKILL.md +1 -1
  38. package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +3 -2
  39. package/skills/cicd/google-cloud/SKILL.md +1 -1
  40. package/skills/cicd/mergify/SKILL.md +1 -1
  41. package/skills/cicd/podman/SKILL.md +1 -1
  42. package/skills/content/article-review/SKILL.md +1 -1
  43. package/skills/content/article-to-image/SKILL.md +1 -1
  44. package/skills/content/flowchart-svg/SKILL.md +1 -1
  45. package/skills/content/image-gen/SKILL.md +1 -1
  46. package/skills/content/mermaid/SKILL.md +1 -1
  47. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  48. package/skills/content/wechat-publisher/SKILL.md +1 -1
  49. package/skills/content/wechat-search/SKILL.md +1 -1
  50. package/skills/content/xai-search/SKILL.md +1 -1
  51. package/skills/core/distill-writing/SKILL.md +1 -1
  52. package/skills/core/doc-to-md/SKILL.md +1 -1
  53. package/skills/core/editorial-review/SKILL.md +1 -1
  54. package/skills/core/house-style/SKILL.md +1 -1
  55. package/skills/core/plain-language/LICENSE +3 -0
  56. package/skills/core/plain-language/SKILL.md +57 -0
  57. package/skills/core/plain-language/release-check.mjs +93 -0
  58. package/skills/core/polish-cn/SKILL.md +1 -1
  59. package/skills/core/reg-parser/SKILL.md +1 -1
  60. package/skills/core/terminology/SKILL.md +1 -1
  61. package/skills/core/write-doc/SKILL.md +1 -1
  62. package/skills/engineering/better-review/SKILL.md +1 -1
  63. package/skills/engineering/contributing/LICENSE +3 -0
  64. package/skills/engineering/contributing/SKILL.md +70 -0
  65. package/skills/engineering/contributing/references/tracker-conventions.md +18 -0
  66. package/skills/engineering/contributing/release-check.mjs +93 -0
  67. package/skills/engineering/contributing/scripts/persist-conventions-core.mjs +118 -0
  68. package/skills/engineering/contributing/scripts/persist-conventions.mjs +84 -0
  69. package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +118 -0
  70. package/skills/engineering/e2e-test/SKILL.md +1 -1
  71. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  72. package/skills/engineering/engineering-runtime/coherence/workflow.json +29 -14
  73. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -0
  74. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  75. package/skills/engineering/golden-path/SKILL.md +1 -1
  76. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  77. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  78. package/skills/engineering/resolve-release/SKILL.md +1 -1
  79. package/skills/engineering/rush-issues/SKILL.md +1 -1
  80. package/skills/engineering/rush-release/SKILL.md +1 -1
  81. package/skills/engineering/test-quality/SKILL.md +1 -1
  82. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +3 -2
  83. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  84. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  85. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  86. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  87. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  88. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  89. package/skills/risk-compliance/report-writing/SKILL.md +1 -1
  90. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  91. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
@@ -52,7 +52,8 @@ table. `docs/catalog/` is generated from those definitions; never edit it by han
52
52
  collections.", not "0 when none".
53
53
  9. Never restate the column name or write "same as above".
54
54
  10. Cover every case: what the column holds for each kind of row, including when it
55
- is empty; list an enumerated column's values; say so if it is always empty today.
55
+ is empty; list an enumerated column's values in the text and in `values=(...)`;
56
+ say so if it is always empty today.
56
57
  11. Write related columns as a set: a count and its amount, a value and its missing
57
58
  flag, a legacy column and its replacement use the same scope and wording, and any
58
59
  difference is stated in both rows.
@@ -50,7 +50,8 @@ column that says only what is particular to that column.
50
50
  `"同上,30 天窗口"` tell the reader nothing.
51
51
  10. **Cover every case.** Say what the column holds for every kind of row (each
52
52
  transaction type, each branch of the code), including when it is empty. List
53
- the values an enumerated column can take, with a Chinese gloss for English codes.
53
+ the values an enumerated column can take, with a Chinese gloss for English codes,
54
+ and put the same list in `values=(...)` so the data can be checked against it.
54
55
  A column that is always empty or constant today says so.
55
56
  Bad: `"换汇交易为 COMPLETED,内部转账为空。"` (collections and payouts unsaid)
56
57
  Good: `"换汇交易为 COMPLETED;收款、付款和内部转账为空。"`
@@ -41,7 +41,10 @@ CATALOG_FIELDS = (
41
41
  PUBLISHED_COLUMNS = tuple(f.name for f in CATALOG_FIELDS)
42
42
  ```
43
43
 
44
- - `Field(name, <one keyword per language>, source, window=None, unit=None)`.
44
+ - `Field(name, <one keyword per language>, source, window=None, unit=None, values=())`.
45
+ `values` lists the values an enumerated column can take, as distinct strings
46
+ (`values=("是", "否", "无法判定")`); the data-quality skill checks the data against it.
47
+ Never use it on identifiers or free text.
45
48
  - `Table(name, <one keyword per language>, grain_<lang>=..., notes_<lang>=(...))`: a
46
49
  grain for every language, and the same number of notes in every language (notes are
47
50
  optional).
@@ -77,7 +80,8 @@ SELECT ...
77
80
  ```
78
81
 
79
82
  - A directive line is `-- @table <name>` or `-- @field <name>`.
80
- - A key line is exactly two spaces after `-- `, then `key: value`.
83
+ - A key line is exactly two spaces after `-- `, then `key: value`. `values:` takes a
84
+ list split on `;` or `;`.
81
85
  - A continuation line has four or more spaces and joins the previous key with a space.
82
86
  - `notes_<lang>` on `@table` splits on `;` or `;`.
83
87
  - The block ends at the first non-comment line. One `@table` per file.
@@ -49,7 +49,7 @@ DEFAULT_CONFIG = {
49
49
  "tableNames": [],
50
50
  "schemaNames": [],
51
51
  }
52
- FIELD_EXTRAS = ("source", "window", "unit")
52
+ FIELD_EXTRAS = ("source", "window", "unit", "values")
53
53
  LANGUAGE_LABELS = {"en": "English", "zh": "中文", "zh_hant": "繁體中文", "ja": "日本語", "ko": "한국어"}
54
54
  LANGUAGE_RE = re.compile(r"^[a-z]{2}(_[a-z]+)?$")
55
55
  SKIP_DIRS = {"node_modules", "__pycache__", "venv"}
@@ -151,6 +151,7 @@ class FieldDoc:
151
151
  window: str | None
152
152
  unit: str | None
153
153
  line: int
154
+ values: tuple = () # the values an enumerated column can take; checked by data-quality
154
155
 
155
156
 
156
157
  @dataclass
@@ -258,7 +259,14 @@ def build_field(values: dict, languages: list, where: str, line: int) -> FieldDo
258
259
  f"languages configured: {languages}")
259
260
  text = {lang: require_text(values, lang, where, line, f"Field {name!r}") for lang in languages}
260
261
  source = require_text(values, "source", where, line, f"Field {name!r}")
261
- return FieldDoc(name, text, source, (values.get("window") or None), (values.get("unit") or None), line)
262
+ allowed = values.get("values") or ()
263
+ if isinstance(allowed, str):
264
+ allowed = tuple(v.strip() for v in re.split(r"[;;]", allowed))
265
+ if not isinstance(allowed, tuple) or not all(isinstance(v, str) and v != "" for v in allowed):
266
+ raise CatalogError(f"{where}:{line}: Field {name!r}: values must be a tuple of non-empty strings")
267
+ if len(set(allowed)) != len(allowed):
268
+ raise CatalogError(f"{where}:{line}: Field {name!r}: values lists a value twice")
269
+ return FieldDoc(name, text, source, (values.get("window") or None), (values.get("unit") or None), line, allowed)
262
270
 
263
271
 
264
272
  def build_table(values: dict, fields: list, languages: list, where: str, line: int, script: str) -> TableDoc:
@@ -545,15 +553,18 @@ def render_table(table: TableDoc, root: Path, config: dict) -> str:
545
553
  out += ["## Fields", ""]
546
554
  has_window = any(f.window for f in table.fields)
547
555
  has_unit = any(f.unit for f in table.fields)
556
+ has_values = any(f.values for f in table.fields)
548
557
  head = ["Field", *(label(lang) for lang in langs), "Source"]
549
558
  head += ["Window"] if has_window else []
550
559
  head += ["Unit"] if has_unit else []
560
+ head += ["Values"] if has_values else []
551
561
  out.append("| " + " | ".join(head) + " |")
552
562
  out.append("|" + "---|" * len(head))
553
563
  for f in table.fields:
554
564
  row = [f"`{f.name}`", *(cell(f.text[lang]) for lang in langs), cell(f.source)]
555
565
  row += [cell(f.window)] if has_window else []
556
566
  row += [cell(f.unit)] if has_unit else []
567
+ row += [cell(", ".join(f"`{v}`" for v in f.values))] if has_values else []
557
568
  out.append("| " + " | ".join(row) + " |")
558
569
  return "\n".join(out) + "\n"
559
570
 
@@ -1175,7 +1186,7 @@ def code_without_metadata(text: str, suffix: str) -> str:
1175
1186
  def definitions(table: TableDoc | None) -> tuple[dict, tuple]:
1176
1187
  if table is None:
1177
1188
  return {}, ()
1178
- fields = {f.name: (tuple(sorted(f.text.items())), f.source, f.window, f.unit) for f in table.fields}
1189
+ fields = {f.name: (tuple(sorted(f.text.items())), f.source, f.window, f.unit, f.values) for f in table.fields}
1179
1190
  notes = tuple((lang, tuple(n)) for lang, n in sorted(table.notes.items()))
1180
1191
  return fields, (tuple(sorted(table.grain.items())), tuple(sorted(table.text.items())), notes)
1181
1192
 
@@ -1274,6 +1285,7 @@ class Field:
1274
1285
  source: str
1275
1286
  window: str | None = None
1276
1287
  unit: str | None = None
1288
+ values: tuple[str, ...] = ()
1277
1289
 
1278
1290
 
1279
1291
  @dataclass(frozen=True)
@@ -5,7 +5,8 @@
5
5
  // section, so no main-module guard here.
6
6
  //
7
7
  // Byte-identical copies live in test-quality (Testing conventions), github-flow
8
- // (CI conventions, Delivery) and data-catalog (Data catalog conventions);
8
+ // (CI conventions, Delivery), data-catalog (Data catalog conventions) and
9
+ // contributing (Issue tracking);
9
10
  // skills/conventions-sections.test.mjs keeps them identical and proves the sections
10
11
  // coexist in one file.
11
12
  //
@@ -22,7 +23,7 @@ const MENTIONS_CONTRIBUTING = /(?:^|[^\w-])CONTRIBUTING\.md/
22
23
  export const AGENTS_SECTION = [
23
24
  '## Contributing',
24
25
  '',
25
- `Follow [${CONTRIBUTING_FILE}](${CONTRIBUTING_FILE}) when writing, reviewing, or delivering code: tests, CI, data definitions, and what to do after each commit.`,
26
+ `Follow [${CONTRIBUTING_FILE}](${CONTRIBUTING_FILE}) when writing, reviewing, or delivering code, or creating issues: tests, CI, data definitions, issue tracking, and what to do after each commit.`,
26
27
  '',
27
28
  ].join('\n')
28
29
 
@@ -124,6 +124,21 @@ class GenerateTest(unittest.TestCase):
124
124
  self.assertIn("| [`dwd.customer_daily`](dwd.customer_daily.md) | Daily customer collection rollup. | "
125
125
  "one row per (dt, cust_id) | 2 |", index)
126
126
 
127
+ def test_values_render_and_must_be_distinct_strings(self):
128
+ script = PY_SCRIPT.replace('source="Run parameter `dt`."),', 'source="Run parameter `dt`.", values=("a", "b")),')
129
+ root = make_repo({"jobs/a.py": script}, BILINGUAL)
130
+ self.assertEqual(run(root, "generate")[0], 0)
131
+ page = (root / "docs/catalog/dwd.customer_daily.md").read_text(encoding="utf-8")
132
+ self.assertIn("| Unit | Values |", page)
133
+ self.assertIn("| `a`, `b` |", page)
134
+ twice = script.replace('values=("a", "b")', 'values=("a", "a")')
135
+ root = make_repo({"jobs/a.py": twice}, BILINGUAL)
136
+ self.assertIn("values lists a value twice", run(root, "generate")[2])
137
+ sql = SQL_SCRIPT.replace("-- source: dim_merchant.merchant_id", "-- source: dim_merchant.merchant_id\n-- values: m1;m2")
138
+ root = make_repo({"sql/m.sql": sql}, BILINGUAL)
139
+ self.assertEqual(run(root, "generate")[0], 0)
140
+ self.assertIn("`m1`, `m2`", (root / "docs/catalog/ads.merchant_monthly.md").read_text(encoding="utf-8"))
141
+
127
142
  def test_check_passes_when_current_and_fails_after_a_definition_changes(self):
128
143
  root = make_repo({"jobs/dwd/customer_daily.py": PY_SCRIPT}, BILINGUAL)
129
144
  self.assertEqual(run(root, "generate")[0], 0)
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1AboveIO
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: data-quality
3
+ description: "Check a published warehouse table's data against its data-catalog definitions with read-only SELECTs: columns never filled or stuck at one value, counts below zero, ratios outside 0–1, declared values that never appear and undeclared values that do. Then classify each finding as a wrong definition, wrong code, or wrong data. Use when asked whether a table's data is right, to profile or sanity-check a dwd/ads table, to find dead or constant columns, to check a column's values against its documented list, after a pipeline change ships, or when a data-catalog definition and the data might disagree. NOT for validating a Spark change before it ships (pyspark canary and reconciliation), writing definitions (data-catalog), or monitoring dashboards."
4
+ release-version: 0.40.0
5
+ ---
6
+ **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
+ # Data quality
8
+
9
+ Ask the data whether it means what its definitions say. The definitions come from the
10
+ data-catalog skill; this skill turns them into read-only queries, runs them, and
11
+ reports where table, code, and documentation disagree.
12
+
13
+ ## Find your task
14
+
15
+ | Task | Do |
16
+ |---|---|
17
+ | The repo has no `data-catalog.json` or the table has no definitions | Set the repo up and document the table with the data-catalog skill first |
18
+ | Check one or more tables | [Run the checks](#run-the-checks) |
19
+ | Decide what a finding means | [Classify each finding](#classify-each-finding) |
20
+ | What each check does, and how to make a column checkable | [references/checks.md](references/checks.md) |
21
+ | Connect to Presto, Trino, Spark SQL, or another engine | [references/checks.md#runners](references/checks.md#runners) |
22
+
23
+ `<engine>` is `<skill-base-dir>/scripts/data_quality.py`. It reads definitions through
24
+ the repo's own `scripts/data-catalog/data_catalog.py`.
25
+
26
+ ## Rules
27
+
28
+ 1. **Read only.** Every query is one SELECT built by the engine; the SQLite runner
29
+ opens the file read-only. Never add a write to a runner, and never run the checks
30
+ with credentials that can write when read-only ones exist.
31
+ 2. **Bound the scan.** Pass `--where` on the partition column for a recent range, and
32
+ widen only when a finding needs it.
33
+ Example: `--where "dt >= DATE '2026-10-01'"` scanned 333K rows in 18 seconds.
34
+ 3. **Aggregates only.** The checks return counts, distinct counts, minimums, maximums,
35
+ and counts per declared value. Never add a query that returns rows, and never print
36
+ a column's actual value when it could identify a customer.
37
+ 4. **A finding is a lead until you classify it.** Read the code that writes the column
38
+ before saying the data, the code, or the definition is wrong.
39
+ Example: `exchange_share` reached 25 while its unit says ratio: read the formula
40
+ before deciding whether the unit or the code is wrong.
41
+ 5. **Report; never fix in the same step.** List each finding with its classification
42
+ and evidence. Fixing a definition is a data-catalog change; fixing code or data is
43
+ the owner's call. File an issue only when the user asks.
44
+ 6. **Make columns checkable at the source.** A finding that needs a fact the catalog
45
+ lacks (allowed values, a unit) is fixed by adding it to the `Field`, not by
46
+ special-casing the check.
47
+ Example: add `values=("是", "否", "无法判定")` to `agent_risk_status`, then rerun.
48
+
49
+ ## Run the checks
50
+
51
+ ```bash
52
+ python3 <engine> run --table usr.dwd_orders --where "dt >= DATE '2026-10-01'" \
53
+ --key dt --key order_id --runner "python3 tools/presto_query.py" # or --sqlite warehouse.db
54
+ python3 <engine> plan --table usr.dwd_orders --where "..." # just the SQL, to run elsewhere
55
+ python3 <engine> judge --results results.json # judge rows you ran yourself
56
+ ```
57
+
58
+ - Run from the repo root, or pass `--root`.
59
+ - Pass the table's grain as `--key` (one per column): a join that repeats rows breaks
60
+ it, and nothing else catches that.
61
+ - One table per run when tables use different partition columns.
62
+ - Engine errors such as "exceeds the allowed maximum stages": lower `--batch`.
63
+ - A column type the engine cannot count distinct values of (maps, some arrays): `--skip`.
64
+ - Exit 0: no problems. Exit 1: problems found. Exit 2: setup or usage error.
65
+
66
+ ## Classify each finding
67
+
68
+ For each problem, read the column's definition and the code that writes it, then give
69
+ one of:
70
+
71
+ | Class | Means | Example | Next step |
72
+ |---|---|---|---|
73
+ | definition | the code and data agree; the definition is wrong or incomplete | a retention ratio documented as 0–1 that is by design unbounded | fix the `Field` (data-catalog) |
74
+ | code | the code does not compute what the definition says | `transaction_result` hard-coded to `SUCCESS` | report to the owner with the code location |
75
+ | data | the code is right; the source data is wrong or missing | a source that never fills a certificate type | report to the source owner |
76
+ | expected | correct as is, and the definition should say so | a payout-only column that is 0 in a collections-only period | add the reason to the definition so the next run marks it documented |
77
+
78
+ A column documented as always empty whose `source` names a real upstream column is
79
+ still a question for the owner: is the placeholder meant to stay, or is the code
80
+ missing the read?
81
+
82
+ Findings marked `(documented)` already match what the definition says (for example
83
+ "always empty"), version and hash columns, and missing-flags that are 0 on every row:
84
+ mention them, do not chase them.
85
+
86
+ ## Report
87
+
88
+ Give, per table: the range checked, the row count, then each problem as
89
+ `column — finding — class — evidence (file:line or query)`, most serious first.
90
+ End with what you could not classify and why. Never claim a table is clean when a
91
+ check was `not-run` or a column was skipped.
92
+
93
+ ## Done when
94
+
95
+ - Every problem finding has a class and evidence, or is listed as unclassified.
96
+ - Nothing was written to the warehouse, and no row-level value was printed.
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "Data quality"
3
+ short_description: "Read-only checks of a warehouse table's data against its data-catalog definitions, with each finding classified"
@@ -0,0 +1,56 @@
1
+ # Checks, findings, and runners
2
+
3
+ ## What each check reads
4
+
5
+ The engine builds up to three kinds of query per documented table:
6
+
7
+ - **Profile** (one per 40 columns, `--batch`): `COUNT(*)`, and per column `COUNT(col)`
8
+ and `COUNT(DISTINCT col)`; `MIN`/`MAX` for columns whose `unit` is numeric (count,
9
+ ratio, percent, a currency, seconds, days, score) or whose name looks like a flag
10
+ (`*_missing`, `*_flag`, `is_*`, `has_*`).
11
+ - **Key** (with `--key`): counts the grain values that repeat, with a GROUP BY …
12
+ HAVING COUNT(*) > 1 subquery.
13
+ - **Values** (one per column with `values=(...)`): `SELECT col AS value, COUNT(*) AS n …
14
+ GROUP BY col`. Only declare `values` on enumerated columns, never on identifiers.
15
+
16
+ ## Findings
17
+
18
+ | Finding | Fires when | Marked documented when |
19
+ |---|---|---|
20
+ | `empty-table` | no rows in the `--where` range; nothing else is judged | — |
21
+ | `never-filled` | the column is empty in every row | the definition says always empty / 始终为空 |
22
+ | `mostly-empty` | filled in under 1% of rows | — |
23
+ | `constant` | one distinct value in every filled row, with at least 30 rows (`--constant-min-rows`) and the column not named in `--where` | the definition says always/constant/恒为; the name is a version, hash, or source table; a flag column is constant at 0 or N |
24
+ | `negative-value` | minimum below 0 for unit count, seconds, or days | — |
25
+ | `out-of-range` | values outside 0–1 for unit `ratio`, or 0–100 for unit `percent` | — |
26
+ | `unseen-value` | a declared value never appears (and the column has rows) | — |
27
+ | `undeclared-value` | a value appears that `values` does not list (empty values are not counted) | — |
28
+ | `duplicate-key` | a `--key` combination appears in more than one row | — |
29
+ | `not-run` | a query has no result | — |
30
+
31
+ ## Making a column checkable
32
+
33
+ Each check needs a fact in the column's data-catalog `Field`:
34
+
35
+ | To check | Write in the `Field` |
36
+ |---|---|
37
+ | allowed values | `values=("是", "否", "无法判定")`, or `-- values: 是; 否; 无法判定` in SQL |
38
+ | 0–1 range | `unit="ratio"`; use `"percent"` for 0–100; leave a ratio unbounded by design without that unit |
39
+ | never negative | `unit="count"` (or seconds, days) |
40
+ | expected empty or constant | say so in the text: "Always empty until the source fills it." |
41
+
42
+ ## Runners
43
+
44
+ `run --runner CMD` sends one SELECT on stdin per query and reads the rows from stdout:
45
+ a JSON array of objects, JSON lines (one object per line), or CSV with a header row.
46
+ Exit non-zero with a message on stderr when a query fails.
47
+
48
+ | Engine | Runner command shape |
49
+ |---|---|
50
+ | Trino / Presto CLI | `trino --server <url> --catalog hive --output-format JSON` (reads stdin) |
51
+ | Presto REST | a small script that POSTs to `/v1/statement`, follows `nextUri`, prints a JSON array |
52
+ | Spark SQL | a script that runs `spark.sql(stdin).toPandas().to_json(orient="records")` |
53
+ | SQLite (local, tests) | built in: `--sqlite <file>`, opened read-only |
54
+
55
+ Use `--dialect spark` with Spark SQL (backtick identifiers); the default quotes with
56
+ double quotes. Use read-only credentials where the engine offers them.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const DAY_MS = 24 * 60 * 60 * 1000;
9
+ const WEEK_MS = 7 * DAY_MS;
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+
12
+ function readJson(path, fallback = {}) {
13
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return fallback; }
14
+ }
15
+
16
+ function frontmatterValue(text, key) {
17
+ const frontmatter = /^---\n([\s\S]*?)\n---/.exec(text)?.[1] ?? '';
18
+ const match = new RegExp('^' + key + ':\\s*([^\\n]+)$', 'm').exec(frontmatter);
19
+ return match?.[1].trim().replace(/^['"]|['"]$/g, '') ?? null;
20
+ }
21
+
22
+ function repositoryOf(source) {
23
+ const value = String(source ?? '').trim();
24
+ const match = /^(?:git@github\.com:|https:\/\/github\.com\/)?([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?$/.exec(value);
25
+ return match?.[1] ?? null;
26
+ }
27
+
28
+ function stableVersion(value) {
29
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(String(value ?? '').trim());
30
+ return match ? match.slice(1).map(Number) : null;
31
+ }
32
+
33
+ function newer(left, right) {
34
+ for (let index = 0; index < 3; index += 1) {
35
+ if (left[index] !== right[index]) return left[index] > right[index];
36
+ }
37
+ return false;
38
+ }
39
+
40
+ function writeCache(path, value) {
41
+ try {
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ writeFileSync(path, JSON.stringify(value));
44
+ } catch { /* A read-only cache must not block skill use. */ }
45
+ }
46
+
47
+ function main(now = Date.now()) {
48
+ if (process.env.ONEABOVEIO_NO_UPDATE_CHECK) return;
49
+ const skill = readFileSync(join(here, 'SKILL.md'), 'utf8');
50
+ const name = frontmatterValue(skill, 'name');
51
+ const localVersion = stableVersion(frontmatterValue(skill, 'release-version'));
52
+ if (!name || !localVersion) return;
53
+
54
+ const lock = readJson(join(process.env.HOME || homedir(), '.agents', '.skill-lock.json'));
55
+ const entry = lock.skills?.[name];
56
+ const repository = repositoryOf(entry?.sourceUrl || entry?.source);
57
+ if (!repository) return;
58
+
59
+ const cacheRoot = process.env.XDG_CACHE_HOME || join(process.env.HOME || homedir(), '.cache');
60
+ const cachePath = join(cacheRoot, '1aboveio-skills', 'release-check.json');
61
+ const all = readJson(cachePath);
62
+ const cache = all[repository] && typeof all[repository] === 'object' ? all[repository] : {};
63
+
64
+ if (!cache.lastCheck || now - cache.lastCheck >= DAY_MS) {
65
+ const result = spawnSync('gh', ['api', 'repos/' + repository + '/releases/latest', '--jq', '.tag_name'], {
66
+ encoding: 'utf8',
67
+ stdio: ['ignore', 'pipe', 'ignore'],
68
+ timeout: 1500,
69
+ });
70
+ cache.lastCheck = now;
71
+ if (result.status === 0 && stableVersion(result.stdout)) cache.latestTag = result.stdout.trim();
72
+ else delete cache.latestTag;
73
+ all[repository] = cache;
74
+ writeCache(cachePath, all);
75
+ }
76
+
77
+ const latestVersion = stableVersion(cache.latestTag);
78
+ if (!latestVersion || !newer(latestVersion, localVersion)) return;
79
+ if (cache.notifiedTag === cache.latestTag && cache.notifiedAt && now - cache.notifiedAt < WEEK_MS) return;
80
+
81
+ cache.notifiedTag = cache.latestTag;
82
+ cache.notifiedAt = now;
83
+ all[repository] = cache;
84
+ writeCache(cachePath, all);
85
+ const command = `npx skills@1.5.23 add '${repository}#${cache.latestTag}' --global --agent universal claude-code --skill ${name} --yes`;
86
+ process.stdout.write(
87
+ `UPDATE_AVAILABLE: A newer 1AboveIO skills release (${cache.latestTag}) is available; this skill is `
88
+ + `v${localVersion.join('.')}. Ask once whether to refresh ${name}. On approval run \`${command}\`. `
89
+ + 'The refreshed instructions apply next session. Either way, continue the current task and do not ask again this session.\n',
90
+ );
91
+ }
92
+
93
+ try { main(); } catch { /* Update checks are advisory and always fail silent. */ }
@@ -0,0 +1,12 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import test from 'node:test';
6
+
7
+ const scriptsDir = dirname(fileURLToPath(import.meta.url));
8
+
9
+ test('data-quality test_data_quality.py', () => {
10
+ const result = spawnSync('python3', [join(scriptsDir, 'test_data_quality.py')], { encoding: 'utf8' });
11
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
12
+ });