mintmark 0.2.0__tar.gz → 0.3.0__tar.gz

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 (145) hide show
  1. {mintmark-0.2.0 → mintmark-0.3.0}/CHANGELOG.md +48 -0
  2. {mintmark-0.2.0 → mintmark-0.3.0}/PKG-INFO +3 -3
  3. {mintmark-0.2.0 → mintmark-0.3.0}/README.md +2 -2
  4. {mintmark-0.2.0 → mintmark-0.3.0}/packs/example/pack.yaml +1 -1
  5. {mintmark-0.2.0 → mintmark-0.3.0}/pyproject.toml +1 -1
  6. {mintmark-0.2.0 → mintmark-0.3.0}/schemas/manifest.schema.json +3 -3
  7. {mintmark-0.2.0 → mintmark-0.3.0}/schemas/pack.schema.json +3 -1
  8. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/cli.py +124 -9
  9. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/emit/__init__.py +2 -1
  10. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/emit/csv_writer.py +24 -0
  11. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/draws.py +20 -1
  12. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/records.py +30 -9
  13. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/version.py +1 -1
  14. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/lexicons/data/special_descriptors.yaml +27 -41
  15. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/lexicons/denylist.py +20 -2
  16. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/manifest/io.py +67 -0
  17. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/manifest/verify.py +227 -151
  18. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/mint.py +132 -92
  19. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/packs/model.py +22 -0
  20. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/alias/pack.yaml +1 -1
  21. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/anchor/pack.yaml +1 -1
  22. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/bad-name-pattern/pack.yaml +1 -1
  23. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/duplicate-key/pack.yaml +1 -1
  24. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/float-weight/pack.yaml +1 -1
  25. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/merge-key/pack.yaml +1 -1
  26. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/missing-schema-version/pack.yaml +1 -1
  27. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/multiple-documents/pack.yaml +1 -1
  28. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/unknown-field/pack.yaml +1 -1
  29. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/valid/pack.yaml +1 -1
  30. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/wrong-locale/pack.yaml +1 -1
  31. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/test_numeric_resource_limits.py +12 -0
  32. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/test_tamper_detection.py +61 -0
  33. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/pack.yaml +1 -1
  34. {mintmark-0.2.0 → mintmark-0.3.0}/tests/golden/demo-run/DIGESTS.json +5 -1
  35. {mintmark-0.2.0 → mintmark-0.3.0}/tests/golden/demo-run/transaction.jsonl +97 -97
  36. {mintmark-0.2.0 → mintmark-0.3.0}/tests/property/test_records.py +48 -5
  37. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_declared_controls.py +154 -9
  38. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_denylist.py +12 -0
  39. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_emit.py +12 -0
  40. mintmark-0.3.0/tests/unit/test_special_descriptors.py +42 -0
  41. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_workflows.py +5 -1
  42. {mintmark-0.2.0 → mintmark-0.3.0}/.gitignore +0 -0
  43. {mintmark-0.2.0 → mintmark-0.3.0}/LICENSE +0 -0
  44. {mintmark-0.2.0 → mintmark-0.3.0}/NOTICE +0 -0
  45. {mintmark-0.2.0 → mintmark-0.3.0}/assets/brand/mintmark-logo.png +0 -0
  46. {mintmark-0.2.0 → mintmark-0.3.0}/assets/brand/mintmark-logo.svg +0 -0
  47. {mintmark-0.2.0 → mintmark-0.3.0}/assets/denylist/institutions-tr.txt +0 -0
  48. {mintmark-0.2.0 → mintmark-0.3.0}/assets/readme/family-topology.png +0 -0
  49. {mintmark-0.2.0 → mintmark-0.3.0}/assets/readme/family-topology.svg +0 -0
  50. {mintmark-0.2.0 → mintmark-0.3.0}/assets/readme/mint-pipeline.png +0 -0
  51. {mintmark-0.2.0 → mintmark-0.3.0}/assets/readme/mint-pipeline.svg +0 -0
  52. {mintmark-0.2.0 → mintmark-0.3.0}/assets/tables/CHECKSUMS +0 -0
  53. {mintmark-0.2.0 → mintmark-0.3.0}/assets/tables/balances.json +0 -0
  54. {mintmark-0.2.0 → mintmark-0.3.0}/assets/tables/txn_amounts.json +0 -0
  55. {mintmark-0.2.0 → mintmark-0.3.0}/packs/example/README.md +0 -0
  56. {mintmark-0.2.0 → mintmark-0.3.0}/packs/example/fields/customer.yaml +0 -0
  57. {mintmark-0.2.0 → mintmark-0.3.0}/packs/example/fields/transaction.yaml +0 -0
  58. {mintmark-0.2.0 → mintmark-0.3.0}/packs/example/recipes/demo.yaml +0 -0
  59. {mintmark-0.2.0 → mintmark-0.3.0}/packs/example/templates/txn_descriptions/set.yaml +0 -0
  60. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/__init__.py +0 -0
  61. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/annotate/__init__.py +0 -0
  62. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/annotate/render.py +0 -0
  63. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/annotate/sidecar.py +0 -0
  64. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/annotate/spans.py +0 -0
  65. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/annotate/taxonomy.py +0 -0
  66. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/api.py +0 -0
  67. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/emit/canonical.py +0 -0
  68. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/emit/writer.py +0 -0
  69. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/__init__.py +0 -0
  70. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/fold.py +0 -0
  71. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/prng.py +0 -0
  72. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/streams.py +0 -0
  73. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/tables.py +0 -0
  74. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/engine/templates.py +0 -0
  75. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/identifiers/__init__.py +0 -0
  76. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/identifiers/email.py +0 -0
  77. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/identifiers/iban.py +0 -0
  78. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/identifiers/pan.py +0 -0
  79. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/identifiers/phone.py +0 -0
  80. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/identifiers/policy.py +0 -0
  81. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/identifiers/tckn.py +0 -0
  82. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/identifiers/vkn.py +0 -0
  83. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/lexicons/__init__.py +0 -0
  84. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/lexicons/data/given_names_tr.yaml +0 -0
  85. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/lexicons/data/occupations_tr.yaml +0 -0
  86. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/lexicons/data/provinces_tr.yaml +0 -0
  87. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/lexicons/data/street_parts_tr.yaml +0 -0
  88. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/lexicons/data/surnames_tr.yaml +0 -0
  89. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/manifest/__init__.py +0 -0
  90. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/manifest/document.py +0 -0
  91. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/manifest/safety.py +0 -0
  92. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/manifest/sums.py +0 -0
  93. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/packs/__init__.py +0 -0
  94. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/packs/digest.py +0 -0
  95. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/packs/loader.py +0 -0
  96. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/packs/semver.py +0 -0
  97. {mintmark-0.2.0 → mintmark-0.3.0}/src/mintmark/py.typed +0 -0
  98. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/empty-file/pack.yaml +0 -0
  99. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/non-mapping-root/pack.yaml +0 -0
  100. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/open-ended-requires-core/pack.yaml +0 -0
  101. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/packs/tab-indentation/pack.yaml +0 -0
  102. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/test_pack_rejection.py +0 -0
  103. {mintmark-0.2.0 → mintmark-0.3.0}/tests/adversarial/test_template_grammar.py +0 -0
  104. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/fields/account.yaml +0 -0
  105. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/fields/card.yaml +0 -0
  106. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/fields/complaint_ticket.yaml +0 -0
  107. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/fields/customer.yaml +0 -0
  108. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/fields/kyc_note.yaml +0 -0
  109. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/fields/support_transcript.yaml +0 -0
  110. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/fields/transaction.yaml +0 -0
  111. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/lexicons/ratings.yaml +0 -0
  112. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/recipes/full.yaml +0 -0
  113. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/templates/complaints/set.yaml +0 -0
  114. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/templates/kyc_notes/set.yaml +0 -0
  115. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/pack/templates/transcripts/set.yaml +0 -0
  116. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conformance/test_contract_coverage.py +0 -0
  117. {mintmark-0.2.0 → mintmark-0.3.0}/tests/conftest.py +0 -0
  118. {mintmark-0.2.0 → mintmark-0.3.0}/tests/golden/demo-run/customer.jsonl +0 -0
  119. {mintmark-0.2.0 → mintmark-0.3.0}/tests/golden/demo-run/transaction.labels.jsonl +0 -0
  120. {mintmark-0.2.0 → mintmark-0.3.0}/tests/golden/prng_vectors.json +0 -0
  121. {mintmark-0.2.0 → mintmark-0.3.0}/tests/golden/test_golden_bytes.py +0 -0
  122. {mintmark-0.2.0 → mintmark-0.3.0}/tests/property/test_draws.py +0 -0
  123. {mintmark-0.2.0 → mintmark-0.3.0}/tests/property/test_fold_and_email.py +0 -0
  124. {mintmark-0.2.0 → mintmark-0.3.0}/tests/property/test_render.py +0 -0
  125. {mintmark-0.2.0 → mintmark-0.3.0}/tests/property/test_safe_mode_purity.py +0 -0
  126. {mintmark-0.2.0 → mintmark-0.3.0}/tests/property/test_span_alignment.py +0 -0
  127. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_canary.py +0 -0
  128. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_cli.py +0 -0
  129. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_community_mirrors.py +0 -0
  130. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_mint_invariants.py +0 -0
  131. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_module_boundaries.py +0 -0
  132. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_no_libm.py +0 -0
  133. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_pack_digest.py +0 -0
  134. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_packaging.py +0 -0
  135. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_prng.py +0 -0
  136. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_prose_lint.py +0 -0
  137. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_readme_mirror.py +0 -0
  138. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_repository_inventory.py +0 -0
  139. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_streams.py +0 -0
  140. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_tables.py +0 -0
  141. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_taxonomy.py +0 -0
  142. {mintmark-0.2.0 → mintmark-0.3.0}/tests/unit/test_toolchain.py +0 -0
  143. {mintmark-0.2.0 → mintmark-0.3.0}/tools/canary.py +0 -0
  144. {mintmark-0.2.0 → mintmark-0.3.0}/tools/gen_tables.py +0 -0
  145. {mintmark-0.2.0 → mintmark-0.3.0}/tools/mdlint.py +0 -0
@@ -11,6 +11,46 @@ output for a fixed set of inputs. A change that alters emitted bytes for a fixed
11
11
  seed is a major version event even when no signature changed, because it breaks
12
12
  the reproducibility of every published manifest.
13
13
 
14
+ ## 0.3.0 - 2026-08-23
15
+
16
+ ### Security
17
+
18
+ - HEALTH descriptors are condition classes only; treatment, surgery, therapy,
19
+ appointment, and examination phrases are no longer emitted under that label.
20
+ - ETHNICITY no longer labels language/accommodation requests, and SEXUAL_LIFE no
21
+ longer labels marital or family-status phrases. Independent tests define the
22
+ forbidden semantic boundaries rather than trusting the descriptor file as its
23
+ own oracle.
24
+ - CSV emission rejects string cells whose first meaningful character is a
25
+ spreadsheet formula trigger (`=`, `+`, `-`, or `@`), including whitespace- and
26
+ control-prefixed forms. It fails closed instead of changing the source value or
27
+ emitting active spreadsheet content.
28
+
29
+ The descriptor corrections move document bytes for fixed seeds and therefore
30
+ constitute a version event under this project's reproducibility contract.
31
+
32
+ ## 0.2.1 - 2026-08-23
33
+
34
+ ### Security
35
+
36
+ - Template, enum, and reference-distribution weights are scaled once per
37
+ declaration rather than reparsed in record-count-multiplied hot loops. Pack
38
+ entity lexicon references are unique and bounded, and their surfaces are
39
+ composed once per mint.
40
+ - Mint generation streams records and label sidecars into the private staging
41
+ directory. It retains only reference identifiers, enforces per-file and
42
+ aggregate output-byte budgets, and still commits atomically after the safe
43
+ identifier invariant succeeds.
44
+ - Dataset verification streams JSONL, CSV, and label sidecars under line, file,
45
+ aggregate-byte, record, and diagnostic-count budgets. Semantic claims are
46
+ accumulated without retaining every parsed record.
47
+ - Validator identifier mode now requires an affirmative caller choice even when
48
+ a recipe requires it. An omitted CLI or API policy is always `safe`; recipe
49
+ binding and exact-policy reproduction remain enforced.
50
+
51
+ No generated record or sidecar bytes move for the same pack, recipe, seed,
52
+ format, and explicit policy. Manifests record the patch engine version.
53
+
14
54
  ## 0.2.0 - 2026-08-23
15
55
 
16
56
  A security and conformance review of the three sector packs found one defect
@@ -319,3 +359,11 @@ and the name was still provisional. Both changed later the same day, in 0.1.1 an
319
359
  in the trademark clearance recorded in TRADEMARKS.md. This note is left as it was
320
360
  written rather than edited, because a changelog that quietly updates itself stops
321
361
  being a record.
362
+ - Denylist comparisons now use compatibility normalization and remove combining
363
+ and default-ignorable characters, closing NFD, full-width, and zero-width
364
+ obfuscation bypasses without changing emitted span offsets.
365
+ - Relationship allocation now enforces declared per-parent bounds and rejects
366
+ infeasible recipe totals before generation.
367
+ - `packcheck` now exhaustively scans lexicons, field literals and parameters,
368
+ and every bounded template branch for denylisted entities and checksum-valid
369
+ identifier literals; its mini-mints preserve relationship constraints.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mintmark
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Deterministic, fully synthetic, Turkish-first labeled datasets with a provenance manifest.
5
5
  Author: Mintmark contributors
6
6
  License-Expression: Apache-2.0
@@ -33,9 +33,9 @@ Description-Content-Type: text/markdown
33
33
 
34
34
  <p align="center">
35
35
  <a href="https://github.com/lokomotifai/mintmark/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/lokomotifai/mintmark/ci.yml?branch=main&amp;style=flat-square&amp;label=CI"></a>
36
- <img alt="905 tests" src="https://img.shields.io/badge/tests-905-3C873A?style=flat-square">
36
+ <img alt="925 tests" src="https://img.shields.io/badge/tests-925-3C873A?style=flat-square">
37
37
  <img alt="18 invariants, each with a named test" src="https://img.shields.io/badge/invariants-18%20tested-3C873A?style=flat-square">
38
- <a href="https://github.com/lokomotifai/mintmark/releases/tag/v0.2.0"><img alt="Release v0.2.0" src="https://img.shields.io/badge/release-v0.2.0-3C873A?style=flat-square"></a>
38
+ <a href="https://github.com/lokomotifai/mintmark/releases/tag/v0.3.0"><img alt="Release v0.3.0" src="https://img.shields.io/badge/release-v0.3.0-3C873A?style=flat-square"></a>
39
39
  <a href="https://pypi.org/project/mintmark/"><img alt="On PyPI" src="https://img.shields.io/pypi/v/mintmark?style=flat-square&amp;label=PyPI&amp;color=3C873A"></a>
40
40
  <a href="LICENSE"><img alt="Apache-2.0 license" src="https://img.shields.io/badge/license-Apache--2.0-3B3F46?style=flat-square"></a>
41
41
  </p>
@@ -13,9 +13,9 @@
13
13
 
14
14
  <p align="center">
15
15
  <a href="https://github.com/lokomotifai/mintmark/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/lokomotifai/mintmark/ci.yml?branch=main&amp;style=flat-square&amp;label=CI"></a>
16
- <img alt="905 tests" src="https://img.shields.io/badge/tests-905-3C873A?style=flat-square">
16
+ <img alt="925 tests" src="https://img.shields.io/badge/tests-925-3C873A?style=flat-square">
17
17
  <img alt="18 invariants, each with a named test" src="https://img.shields.io/badge/invariants-18%20tested-3C873A?style=flat-square">
18
- <a href="https://github.com/lokomotifai/mintmark/releases/tag/v0.2.0"><img alt="Release v0.2.0" src="https://img.shields.io/badge/release-v0.2.0-3C873A?style=flat-square"></a>
18
+ <a href="https://github.com/lokomotifai/mintmark/releases/tag/v0.3.0"><img alt="Release v0.3.0" src="https://img.shields.io/badge/release-v0.3.0-3C873A?style=flat-square"></a>
19
19
  <a href="https://pypi.org/project/mintmark/"><img alt="On PyPI" src="https://img.shields.io/pypi/v/mintmark?style=flat-square&amp;label=PyPI&amp;color=3C873A"></a>
20
20
  <a href="LICENSE"><img alt="Apache-2.0 license" src="https://img.shields.io/badge/license-Apache--2.0-3B3F46?style=flat-square"></a>
21
21
  </p>
@@ -1,7 +1,7 @@
1
1
  schema_version: 1
2
2
  name: mintmark-example
3
3
  version: 0.1.0
4
- requires_core: ">=0.2,<0.3"
4
+ requires_core: ">=0.3,<0.4"
5
5
  locale: tr-TR
6
6
  allowed_identifier_policies: [safe, validator]
7
7
  description: Fixture pack for the quickstart and the test suite. Not a sector pack.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mintmark"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Deterministic, fully synthetic, Turkish-first labeled datasets with a provenance manifest."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<3.13"
@@ -211,7 +211,7 @@
211
211
  "records": {
212
212
  "type": "integer",
213
213
  "minimum": 0,
214
- "maximum": 100000
214
+ "maximum": 1000000
215
215
  }
216
216
  }
217
217
  }
@@ -292,12 +292,12 @@
292
292
  "target": {
293
293
  "type": "integer",
294
294
  "minimum": 0,
295
- "maximum": 100000
295
+ "maximum": 1000000
296
296
  },
297
297
  "achieved": {
298
298
  "type": "integer",
299
299
  "minimum": 0,
300
- "maximum": 100000
300
+ "maximum": 1000000
301
301
  },
302
302
  "met": {
303
303
  "type": "boolean"
@@ -76,6 +76,8 @@
76
76
  "additionalProperties": {
77
77
  "type": "array",
78
78
  "minItems": 1,
79
+ "maxItems": 128,
80
+ "uniqueItems": true,
79
81
  "items": {
80
82
  "type": "string",
81
83
  "pattern": "^[a-z][a-z0-9_]*$"
@@ -287,7 +289,7 @@
287
289
  "safe",
288
290
  "validator"
289
291
  ],
290
- "description": "The policy this recipe is minted under. Binding: a mint that asks for a different one is refused. Absent means the caller chooses, defaulting to safe."
292
+ "description": "The policy this recipe is minted under. Binding: a mint that asks for a different one is refused. Validator mode still requires an explicit caller opt-in; an omitted caller choice is always safe."
291
293
  }
292
294
  }
293
295
  },
@@ -31,7 +31,18 @@ from pathlib import Path
31
31
  from typing import Any
32
32
 
33
33
  from mintmark.annotate import Label, pin_digest
34
- from mintmark.identifiers import CHECKSUMMED
34
+ from mintmark.engine.templates import (
35
+ Alternation,
36
+ EntitySlot,
37
+ FieldSlot,
38
+ IdentifierSlot,
39
+ LexiconSlot,
40
+ Literal,
41
+ Optional,
42
+ parse_template,
43
+ )
44
+ from mintmark.identifiers import ALL_ENGINES, CHECKSUMMED
45
+ from mintmark.lexicons import load as load_denylist
35
46
  from mintmark.manifest import (
36
47
  MANIFEST_FILENAME,
37
48
  SUMS_FILENAME,
@@ -46,7 +57,8 @@ from mintmark.manifest.io import (
46
57
  DatasetIOError,
47
58
  DatasetReader,
48
59
  )
49
- from mintmark.mint import MintError, mint, packaged_pack_dir, resolve_pack, schema_dir
60
+ from mintmark.manifest.safety import identifier_candidates, scalar_texts
61
+ from mintmark.mint import MintError, asset_dir, mint, packaged_pack_dir, resolve_pack, schema_dir
50
62
  from mintmark.packs.loader import PackError
51
63
  from mintmark.packs.model import load_pack
52
64
 
@@ -98,8 +110,8 @@ def build_parser() -> argparse.ArgumentParser:
98
110
  mint_parser.add_argument(
99
111
  "--identifier-policy",
100
112
  choices=["safe", "validator"],
101
- default=None,
102
- help="only needed when the recipe does not name one; a recipe that does, decides",
113
+ default="safe",
114
+ help="safe by default; validator requires an explicit caller opt-in",
103
115
  )
104
116
  mint_parser.add_argument("--format", choices=["jsonl", "csv"], default="jsonl")
105
117
  mint_parser.add_argument("--json", action="store_true")
@@ -433,17 +445,116 @@ def _locate_pack(directory: Path, name: str) -> Path | None:
433
445
  return None
434
446
 
435
447
 
448
+ MAX_TEMPLATE_SAFETY_EXPANSIONS = 10_000
449
+
450
+
451
+ def _literal_expansions(nodes: tuple[Any, ...]) -> tuple[str, ...]:
452
+ """Enumerate every literal grammar branch under a strict cross-product cap."""
453
+ outputs = [""]
454
+ for node in nodes:
455
+ variants: tuple[str, ...]
456
+ match node:
457
+ case Literal(text=text):
458
+ variants = (text,)
459
+ case FieldSlot() | EntitySlot() | IdentifierSlot() | LexiconSlot():
460
+ variants = (" ",)
461
+ case Alternation(branches=branches):
462
+ variants = tuple(
463
+ rendering for branch in branches for rendering in _literal_expansions(branch)
464
+ )
465
+ case Optional(body=body):
466
+ variants = ("", *_literal_expansions(body))
467
+ case _:
468
+ raise AssertionError(f"unknown template node {node!r}")
469
+ if len(outputs) * len(variants) > MAX_TEMPLATE_SAFETY_EXPANSIONS:
470
+ raise ValueError(
471
+ f"template expands beyond {MAX_TEMPLATE_SAFETY_EXPANSIONS} safety branches"
472
+ )
473
+ outputs = [prefix + suffix for prefix in outputs for suffix in variants]
474
+ return tuple(outputs)
475
+
476
+
477
+ def _pack_safety(pack_root: Path, loaded: Any) -> tuple[Any, list[str]]:
478
+ """Exhaustively inspect declaration-controlled emitted strings."""
479
+ core = load_denylist(asset_dir("denylist") / "institutions-tr.txt")
480
+ extension_path = pack_root / "lexicons" / "denylist_extension.txt"
481
+ matcher = load_denylist(extension_path) if extension_path.exists() else core
482
+ problems: list[str] = []
483
+ if not matcher.covers(core):
484
+ missing = sorted(matcher.missing_from(core))[:5]
485
+ problems.append(f"denylist extension omits core entries: {missing}")
486
+
487
+ surfaces: list[tuple[str, str]] = []
488
+ for name, values in loaded.lexicons.items():
489
+ surfaces.extend((f"lexicon {name}", value) for value in values)
490
+ for record_type in loaded.record_types:
491
+ for field in record_type.fields:
492
+ source = f"field {record_type.type_name}.{field.name}"
493
+ if field.generator_kind == "literal":
494
+ surfaces.append((source, field.generator_argument))
495
+ surfaces.extend((source, text) for text in scalar_texts(field.params))
496
+ for set_name, entries in loaded.template_sets.items():
497
+ for entry in entries:
498
+ nodes = parse_template(
499
+ entry.text,
500
+ template_id=entry.id,
501
+ known_labels=frozenset(label.value for label in Label),
502
+ known_identifiers=frozenset(ALL_ENGINES),
503
+ known_lexicons=frozenset(loaded.lexicons),
504
+ )
505
+ try:
506
+ renderings = _literal_expansions(nodes)
507
+ except ValueError as exc:
508
+ problems.append(f"template {set_name}/{entry.id}: {exc}")
509
+ continue
510
+ surfaces.extend((f"template {set_name}/{entry.id}", text) for text in renderings)
511
+
512
+ for source, surface in surfaces:
513
+ for hit in matcher.scan(surface):
514
+ problems.append(f"{source}: {hit.render()}")
515
+ for candidate in identifier_candidates(surface):
516
+ valid_as = [
517
+ name for name, engine in CHECKSUMMED.items() if engine.is_checksum_valid(candidate)
518
+ ]
519
+ if valid_as:
520
+ problems.append(
521
+ f"{source}: checksum-valid {'/'.join(valid_as)} literal {candidate!r} "
522
+ "is forbidden by safe policy"
523
+ )
524
+ return matcher, problems
525
+
526
+
527
+ def _mini_record_counts(loaded: Any, recipe: Any) -> dict[str, int]:
528
+ """Shrink a valid recipe while retaining every relationship bound."""
529
+ selected = {name: min(25, count) for name, count in recipe.records.items()}
530
+ for record_type in loaded.record_types:
531
+ for field in record_type.fields:
532
+ if field.ref is None:
533
+ continue
534
+ if selected.get(record_type.type_name, 0) == 0:
535
+ continue
536
+ parents = selected.get(field.ref.parent, 0)
537
+ low = parents * min(field.ref.counts)
538
+ high = parents * max(field.ref.counts)
539
+ selected[record_type.type_name] = min(
540
+ max(selected.get(record_type.type_name, 0), low), high
541
+ )
542
+ return selected
543
+
544
+
436
545
  def _cmd_packcheck(args: argparse.Namespace) -> int:
437
546
  pack_root = resolve_pack(args.pack)
438
547
  loaded = load_pack(pack_root)
439
548
 
440
549
  problems: list[str] = []
441
550
  warnings: list[str] = []
551
+ matcher, safety_problems = _pack_safety(pack_root, loaded)
552
+ problems.extend(safety_problems)
442
553
 
443
554
  with tempfile.TemporaryDirectory() as temporary:
444
555
  for recipe_name, recipe in sorted(loaded.recipes.items()):
445
556
  out = Path(temporary) / f"mini-{recipe_name}"
446
- overrides = {name: min(25, count) for name, count in recipe.records.items()}
557
+ overrides = _mini_record_counts(loaded, recipe)
447
558
  try:
448
559
  mint(
449
560
  pack=pack_root,
@@ -467,12 +578,16 @@ def _cmd_packcheck(args: argparse.Namespace) -> int:
467
578
  problems.extend(f"recipe {recipe_name}: {p}" for p in report.problems)
468
579
 
469
580
  document = read_manifest(out)
581
+ for output in document["outputs"]:
582
+ output_path = out / output["path"]
583
+ for hit in matcher.scan(output_path.read_text(encoding="utf-8")):
584
+ problems.append(f"recipe {recipe_name}: {output['path']}: {hit.render()}")
470
585
  for stat in document["stats"]["coverage_targets"]:
471
- scaled = stat["target"] * 25
472
- if stat["achieved"] * max(1, sum(recipe.records.values())) < scaled:
473
- warnings.append(
586
+ scaled_target = stat["target"] * max(1, sum(overrides.values()))
587
+ if stat["achieved"] * max(1, sum(recipe.records.values())) < scaled_target:
588
+ problems.append(
474
589
  f"recipe {recipe_name}: coverage target {stat['label']}="
475
- f"{stat['target']} may be infeasible at the declared counts"
590
+ f"{stat['target']} is infeasible at the declared counts"
476
591
  )
477
592
  shutil.rmtree(out, ignore_errors=True)
478
593
 
@@ -10,12 +10,13 @@ from mintmark.emit.canonical import (
10
10
  FloatInOutputError,
11
11
  render_record,
12
12
  )
13
- from mintmark.emit.csv_writer import csv_header, render_csv_row
13
+ from mintmark.emit.csv_writer import SpreadsheetFormulaError, csv_header, render_csv_row
14
14
  from mintmark.emit.writer import StagedOutput, staged_output
15
15
 
16
16
  __all__ = [
17
17
  "CanonicalEncoder",
18
18
  "FloatInOutputError",
19
+ "SpreadsheetFormulaError",
19
20
  "StagedOutput",
20
21
  "csv_header",
21
22
  "render_csv_row",
@@ -13,9 +13,14 @@ from __future__ import annotations
13
13
 
14
14
  import csv
15
15
  import io
16
+ import re
16
17
  from typing import Any
17
18
 
18
19
 
20
+ class SpreadsheetFormulaError(ValueError):
21
+ """A string cell would be interpreted as active content by spreadsheets."""
22
+
23
+
19
24
  def csv_header(field_order: tuple[str, ...]) -> str:
20
25
  """The header row, in declaration order, RFC 4180 quoted, LF terminated."""
21
26
  return _render_row(list(field_order))
@@ -40,10 +45,29 @@ def render_csv_row(record: dict[str, Any], field_order: tuple[str, ...]) -> str:
40
45
  f"field {name!r} carries a {type(value).__name__}, which has no CSV "
41
46
  "representation. Emit this record type as JSONL, or flatten the field."
42
47
  )
48
+ if isinstance(value, str) and _formula_prefix(value) is not None:
49
+ raise SpreadsheetFormulaError(
50
+ f"field {name!r} begins with spreadsheet formula trigger "
51
+ f"{_formula_prefix(value)!r}; CSV emission refuses active cells"
52
+ )
43
53
  cells.append("" if value is None else str(value))
44
54
  return _render_row(cells)
45
55
 
46
56
 
57
+ def _formula_prefix(value: str) -> str | None:
58
+ position = 0
59
+ while position < len(value) and (value[position].isspace() or ord(value[position]) < 0x20):
60
+ position += 1
61
+ candidate = value[position:]
62
+ if re.fullmatch(r"[+-]\d+(?:\.\d+)?", candidate) or re.fullmatch(
63
+ r"\+90 5\d{2} \d{3} \d{2} \d{2}", candidate
64
+ ):
65
+ return None
66
+ if candidate.startswith(("=", "+", "-", "@")):
67
+ return candidate[0]
68
+ return None
69
+
70
+
47
71
  def _render_row(cells: list[str]) -> str:
48
72
  buffer = io.StringIO()
49
73
  writer = csv.writer(buffer, lineterminator="\n", quoting=csv.QUOTE_MINIMAL)
@@ -16,6 +16,7 @@ tolerate.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ from collections.abc import Sequence
19
20
  from dataclasses import dataclass
20
21
  from decimal import Decimal, InvalidOperation
21
22
 
@@ -126,8 +127,26 @@ def weighted_index(stream: SplitMix64, weights: list[str]) -> int:
126
127
  weights first would change which index a given draw selects and therefore
127
128
  change emitted bytes for a fixed seed.
128
129
  """
129
- scaled = scale_weights(weights)
130
+ return weighted_index_scaled(stream, scale_weights(weights))
131
+
132
+
133
+ def weighted_index_scaled(stream: SplitMix64, scaled: Sequence[int]) -> int:
134
+ """Draw from weights that were validated and scaled at declaration load time.
135
+
136
+ Generation hot paths call this form so a pack with thousands of choices
137
+ cannot multiply decimal parsing by its record count. The caller must retain
138
+ declaration order; doing so preserves the exact draw made by
139
+ :func:`weighted_index`.
140
+ """
141
+ if not scaled:
142
+ raise ValueError("scaled weights must not be empty")
143
+ if any(
144
+ not isinstance(weight, int) or isinstance(weight, bool) or weight < 0 for weight in scaled
145
+ ):
146
+ raise ValueError("scaled weights must be non-negative integers")
130
147
  total = sum(scaled)
148
+ if total <= 0:
149
+ raise ValueError("scaled weights sum to zero")
131
150
  target = bounded(stream, total)
132
151
  cumulative = 0
133
152
  for index, weight in enumerate(scaled):
@@ -25,7 +25,7 @@ from dataclasses import dataclass
25
25
  from dataclasses import field as dataclass_field
26
26
  from typing import Any
27
27
 
28
- from mintmark.engine.draws import bounded, weighted_index
28
+ from mintmark.engine.draws import bounded, scale_weights, weighted_index, weighted_index_scaled
29
29
  from mintmark.engine.streams import StreamFactory
30
30
 
31
31
  # One record is an ordered mapping of field name to emitted value. Order is the
@@ -118,9 +118,9 @@ def assign_children(
118
118
  """Return one parent index per child, honoring the count distribution.
119
119
 
120
120
  The distribution says how many children each parent should have. The recipe
121
- says how many children exist. When they disagree, the distribution shapes the
122
- allocation and the recipe's count wins on total, with the remainder walked
123
- deterministically from parent zero upward.
121
+ says how many children exist. The requested total must fit the declared
122
+ per-parent minimum and maximum; deterministic adjustment never crosses those
123
+ bounds.
124
124
  """
125
125
  if parent_count <= 0:
126
126
  if child_count > 0:
@@ -129,11 +129,27 @@ def assign_children(
129
129
  "a reference cannot dangle"
130
130
  )
131
131
  return []
132
+ if child_count == 0:
133
+ # A recipe may explicitly disable a record type. Cardinality applies
134
+ # when that relationship is present, not to a type the recipe omits.
135
+ return []
136
+
137
+ minimum = min(counts)
138
+ maximum = max(counts)
139
+ minimum_total = parent_count * minimum
140
+ maximum_total = parent_count * maximum
141
+ if not minimum_total <= child_count <= maximum_total:
142
+ raise ValueError(
143
+ f"{site}: {child_count} children cannot satisfy {minimum}..{maximum} "
144
+ f"children for each of {parent_count} parents; feasible total is "
145
+ f"{minimum_total}..{maximum_total}"
146
+ )
132
147
 
133
148
  per_parent: list[int] = []
149
+ scaled_weights = scale_weights(list(weights))
134
150
  for parent_index in range(parent_count):
135
151
  stream = factory.stream(f"{site}/{parent_index}/count")
136
- per_parent.append(counts[weighted_index(stream, list(weights))])
152
+ per_parent.append(counts[weighted_index_scaled(stream, scaled_weights)])
137
153
 
138
154
  total = sum(per_parent)
139
155
  if total > child_count:
@@ -143,21 +159,26 @@ def assign_children(
143
159
  for parent_index in range(parent_count - 1, -1, -1):
144
160
  if surplus == 0:
145
161
  break
146
- take = min(per_parent[parent_index], surplus)
162
+ take = min(per_parent[parent_index] - minimum, surplus)
147
163
  per_parent[parent_index] -= take
148
164
  surplus -= take
149
165
  elif total < child_count:
150
166
  deficit = child_count - total
151
167
  parent_index = 0
152
168
  while deficit > 0:
153
- per_parent[parent_index % parent_count] += 1
169
+ selected = parent_index % parent_count
154
170
  parent_index += 1
155
- deficit -= 1
171
+ if per_parent[selected] < maximum:
172
+ per_parent[selected] += 1
173
+ deficit -= 1
174
+
175
+ if sum(per_parent) != child_count:
176
+ raise AssertionError(f"{site}: bounded relationship allocation did not converge")
156
177
 
157
178
  assignment: list[int] = []
158
179
  for parent_index, count in enumerate(per_parent):
159
180
  assignment.extend([parent_index] * count)
160
- return assignment[:child_count]
181
+ return assignment
161
182
 
162
183
 
163
184
  @dataclass(slots=True)
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
@@ -11,24 +11,18 @@ labels:
11
11
  values:
12
12
  - kronik rahatsızlık
13
13
  - geçici iş göremezlik
14
- - rutin kontrol
15
- - istirahat raporu
16
- - kontrol randevusu
17
- - ameliyat sonrası izin
18
- - sağlık raporu
19
- - tedavi süreci
20
- - fizik tedavi programı
21
- - doğum öncesi izin
22
- - iş kazası sonrası rapor
23
- - sevk belgesi
24
- - kontrol izni
25
- - rapor uzatma talebi
26
- - iş göremezlik bildirimi
27
14
  - engel durumu beyanı
28
- - refakat izni
29
- - periyodik muayene kaydı
30
- - sağlık kurulu belgesi
31
- - gebelik izni bildirimi
15
+ - sağlık durumu beyanı
16
+ - mesleki sağlık kısıtı
17
+ - hareket kısıtlılığı beyanı
18
+ - görme desteği ihtiyacı
19
+ - işitme desteği ihtiyacı
20
+ - alerji durumu beyanı
21
+ - solunum hassasiyeti beyanı
22
+ - kas iskelet kısıtı beyanı
23
+ - süreli sağlık kısıtı
24
+ - kalıcı sağlık kısıtı
25
+ - bulaşıcı olmayan rahatsızlık sınıfı
32
26
  RELIGION:
33
27
  note: Observance and accommodation, not belief content.
34
28
  values:
@@ -45,20 +39,16 @@ labels:
45
39
  - inanç temelli mazeret bildirimi
46
40
  - ibadet alanı talebi
47
41
  ETHNICITY:
48
- note: Declared origin and language preference, as an HR or claims form would record them.
42
+ note: Declared ethnic or cultural origin only, as a form would record it.
49
43
  values:
50
44
  - etnik köken beyanı
51
- - anadil tercihi
52
- - iki dilli iletişim talebi
53
- - tercüman talebi
54
- - yerel dil desteği
55
- - köken beyanı formu
56
- - azınlık dili desteği talebi
57
- - yazışma dili tercihi
58
- - kültürel uyum notu
59
- - isim yazımı düzeltme talebi
60
- - çeviri desteği kaydı
61
- - ana dilde bilgilendirme talebi
45
+ - soy kökeni beyanı
46
+ - kültürel köken beyanı
47
+ - etnik kimlik beyanı
48
+ - azınlık grubu aidiyeti beyanı
49
+ - etnik topluluk aidiyeti
50
+ - aile kökeni bölgesi beyanı
51
+ - köken bilgisi formu
62
52
  POLITICAL:
63
53
  note: Fictional parties and civic bodies. No real party is ever named.
64
54
  values:
@@ -77,18 +67,14 @@ labels:
77
67
  SEXUAL_LIFE:
78
68
  note: Form-level references only. Never narrative, never detail.
79
69
  values:
80
- - özel yaşam beyanı
81
- - medeni hâl güncellemesi
82
- - aile durumu bildirimi
83
- - eş bilgisi güncellemesi
84
- - birlikte yaşanan kişi beyanı
85
- - hayat arkadaşı bildirimi
86
- - nikah durumu kaydı
87
- - aile birleşimi talebi
88
- - eş sağlık kapsamı talebi
89
- - boşanma durumu güncellemesi
90
- - birliktelik beyanı formu
91
- - aile içi durum bildirimi
70
+ - cinsel yaşam verisi beyanı
71
+ - cinsel yönelim beyanı
72
+ - mahrem ilişki bilgisi formu
73
+ - cinsel yaşam mahremiyet talebi
74
+ - cinsel yaşama ilişkin veri düzeltme talebi
75
+ - mahrem partner bilgisi beyanı
76
+ - cinsel yaşama ilişkin erişim kısıtı
77
+ - cinsel yaşam verisi silme talebi
92
78
  CRIMINAL:
93
79
  note: Check categories, never findings. 'A record was requested' is a process step; 'a record
94
80
  exists' would be an accusation about a person, and this project does not make those even