pytaut 0.1.0__py3-none-any.whl

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 (88) hide show
  1. pytaut-0.1.0.dist-info/METADATA +174 -0
  2. pytaut-0.1.0.dist-info/RECORD +88 -0
  3. pytaut-0.1.0.dist-info/WHEEL +4 -0
  4. pytaut-0.1.0.dist-info/entry_points.txt +2 -0
  5. pytaut-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. taut/__init__.py +5 -0
  7. taut/analysis/__init__.py +3 -0
  8. taut/analysis/contracts.py +108 -0
  9. taut/analysis/module_analysis.py +31 -0
  10. taut/analysis/project_analyzer.py +80 -0
  11. taut/analysis/project_index.py +132 -0
  12. taut/analysis/python/__init__.py +3 -0
  13. taut/analysis/python/adapter.py +421 -0
  14. taut/analysis/python/expression_summary.py +159 -0
  15. taut/analysis/python/failed_analysis.py +44 -0
  16. taut/analysis/python/language_adapter.py +87 -0
  17. taut/analysis/python/symbol_resolver.py +216 -0
  18. taut/analysis/semantic_model.py +75 -0
  19. taut/cli.py +280 -0
  20. taut/configuration/__init__.py +3 -0
  21. taut/configuration/catalog.py +102 -0
  22. taut/configuration/effective_policy.py +362 -0
  23. taut/configuration/manifest.py +121 -0
  24. taut/configuration/model.py +51 -0
  25. taut/configuration/rule_standard.py +61 -0
  26. taut/configuration/validation.py +20 -0
  27. taut/domain/__init__.py +3 -0
  28. taut/domain/diagnostics.py +32 -0
  29. taut/domain/evaluations.py +81 -0
  30. taut/domain/facts.py +370 -0
  31. taut/domain/findings.py +81 -0
  32. taut/domain/frozen.py +48 -0
  33. taut/domain/ids.py +84 -0
  34. taut/domain/ignores.py +22 -0
  35. taut/domain/issues.py +33 -0
  36. taut/domain/location.py +81 -0
  37. taut/domain/provenance.py +21 -0
  38. taut/domain/reports.py +85 -0
  39. taut/domain/snapshot.py +79 -0
  40. taut/finding_processing/__init__.py +3 -0
  41. taut/finding_processing/finding_processor.py +192 -0
  42. taut/finding_processing/report_builder.py +55 -0
  43. taut/loading/__init__.py +3 -0
  44. taut/loading/boundary_extension_schema.py +43 -0
  45. taut/loading/builtin_catalog.py +49 -0
  46. taut/loading/code_conventions.py +136 -0
  47. taut/loading/config_loader.py +488 -0
  48. taut/loading/configuration_document.py +222 -0
  49. taut/loading/default_configuration.py +59 -0
  50. taut/loading/errors.py +2 -0
  51. taut/loading/inline_ignores.py +81 -0
  52. taut/loading/source_discovery.py +159 -0
  53. taut/policy/__init__.py +3 -0
  54. taut/policy/context.py +44 -0
  55. taut/policy/decision_digest.py +30 -0
  56. taut/policy/engine.py +187 -0
  57. taut/policy/indexes.py +82 -0
  58. taut/policy/registry.py +27 -0
  59. taut/policy/rule.py +75 -0
  60. taut/policy/rules/__init__.py +99 -0
  61. taut/policy/rules/api_contracts.py +484 -0
  62. taut/policy/rules/architecture.py +142 -0
  63. taut/policy/rules/async_safety.py +73 -0
  64. taut/policy/rules/boundary.py +164 -0
  65. taut/policy/rules/catalog_coverage.py +68 -0
  66. taut/policy/rules/classification.py +54 -0
  67. taut/policy/rules/construction_boundaries.py +191 -0
  68. taut/policy/rules/conventions.py +144 -0
  69. taut/policy/rules/enums.py +217 -0
  70. taut/policy/rules/exceptions.py +241 -0
  71. taut/policy/rules/external_calls.py +134 -0
  72. taut/policy/rules/helpers.py +48 -0
  73. taut/policy/rules/ignore_audit.py +35 -0
  74. taut/policy/rules/layer_boundaries.py +440 -0
  75. taut/policy/rules/model_shapes.py +366 -0
  76. taut/policy/rules/persistence.py +370 -0
  77. taut/policy/rules/responsibility_boundaries.py +227 -0
  78. taut/policy/rules/runtime_safety.py +168 -0
  79. taut/policy/rules/security.py +122 -0
  80. taut/policy/rules/session.py +192 -0
  81. taut/policy/rules/test_boundaries.py +110 -0
  82. taut/policy/rules/time_access.py +86 -0
  83. taut/policy/rules/transaction.py +106 -0
  84. taut/policy/scheduler.py +51 -0
  85. taut/py.typed +1 -0
  86. taut/reporting/__init__.py +3 -0
  87. taut/reporting/json.py +117 -0
  88. taut/reporting/text.py +211 -0
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.5
2
+ Name: pytaut
3
+ Version: 0.1.0
4
+ Summary: Deterministic architecture policy checks for Python backends
5
+ Project-URL: Documentation, https://github.com/taewoo-dev/taut#readme
6
+ Project-URL: Issues, https://github.com/taewoo-dev/taut/issues
7
+ Project-URL: Repository, https://github.com/taewoo-dev/taut
8
+ Author-email: TaeWoo Kim <173425210+taewoo-dev@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: architecture,linter,policy,python,static-analysis
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.12
21
+ Description-Content-Type: text/markdown
22
+
23
+ # taut
24
+
25
+ `taut` makes hidden Python backend conventions explicit and blocks only violations it can
26
+ determine reliably. The same source and configuration always produce the same result. It does
27
+ not hard-code the names or directory layout of any company or service.
28
+
29
+ The first PyPI release is being prepared. Until it is published, install directly from GitHub:
30
+
31
+ ```bash
32
+ uv add --dev "pytaut @ git+https://github.com/taewoo-dev/taut.git"
33
+ uv run taut check .
34
+ ```
35
+
36
+ After the PyPI release, installation becomes:
37
+
38
+ ```bash
39
+ uv add --dev pytaut
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ Define repository roles and allowed dependencies in `pyproject.toml`. Strict mode is enabled by
45
+ default, and the built-in maximum file length is 700 lines.
46
+
47
+ ```toml
48
+ [tool.taut]
49
+ strict = true
50
+ source_roots = ["."]
51
+
52
+ [tool.taut.roles]
53
+ router = ["app/router/*.py", "app/router/**/*.py"]
54
+ service = ["app/service/*.py", "app/service/**/*.py"]
55
+
56
+ [tool.taut.allow]
57
+ router = ["router", "service"]
58
+ service = ["service"]
59
+
60
+ [tool.taut.zones]
61
+ test = ["tests/*.py", "tests/**/*.py"]
62
+
63
+ [tool.taut.transaction]
64
+ owner_roles = ["service"]
65
+ session_providers = ["app.database.get_async_session"]
66
+ ```
67
+
68
+ Built-in policies cover external calls, databases, security, DTOs, and schemas. Add only the
69
+ repository-specific differences:
70
+
71
+ ```toml
72
+ [tool.taut.external]
73
+ modules = ["company_sdk"]
74
+ wrappers = ["app.adapters.external_call"]
75
+
76
+ [tool.taut.enum]
77
+ shared_modules = ["app.core.enums"]
78
+ ```
79
+
80
+ ## Commands
81
+
82
+ ```bash
83
+ taut config validate .
84
+ taut check .
85
+ taut check . --verbose
86
+ taut check . --format json
87
+ taut rules
88
+ taut rules ASYNC001
89
+ ```
90
+
91
+ To check another local repository:
92
+
93
+ ```bash
94
+ uvx --no-cache --from /path/to/taut taut check /path/to/project
95
+ ```
96
+
97
+ For a one-time audit that must not modify the target repository, provide an absolute path to an
98
+ external configuration file:
99
+
100
+ ```bash
101
+ taut check /path/to/project --config /path/to/audit-policy.toml
102
+ ```
103
+
104
+ Role patterns and `source_roots` are resolved relative to the target project, not the
105
+ configuration file. The legacy `.policy/policy.toml` format and explicit external configuration
106
+ files remain supported.
107
+
108
+ ## Results
109
+
110
+ The default terminal output prints one finding per line followed by the error and warning totals.
111
+ Long findings wrap to the next indented line. Non-terminal output uses a width of 120 characters;
112
+ override it with an option such as `--width 100`. Use `--verbose` only when you need related
113
+ locations, remediation guidance, decision counts, and the decision digest.
114
+
115
+ - Exit code `0`: no enforced violations
116
+ - Exit code `1`: one or more enforced violations
117
+ - Exit code `2`: invalid configuration, analysis failure, or an enforced rule that could not be
118
+ evaluated
119
+
120
+ When an exception is unavoidable, suppress only the exact rule on the affected line:
121
+
122
+ ```python
123
+ legacy_call() # taut: ignore[ASYNC001]
124
+ ```
125
+
126
+ An ignore without a rule ID, or with an unknown rule ID, is a configuration error. An ignore that
127
+ does not suppress a real violation is reported as `IGNORE001`. File-wide ignores, violation
128
+ baselines, and expiration management are intentionally unsupported.
129
+
130
+ Raw SQL is not generally allowed. Application code should use SQLAlchemy expressions. A necessary
131
+ raw query must pass through a registered shared wrapper configured with `raw_query_roles` and
132
+ `raw_query_wrappers`. Fixed Model `server_default` expressions and partial Index predicates are
133
+ allowed only within `schema_sql_roles` and `schema_sql_argument_names`.
134
+
135
+ Registered raw-query calls must provide `name`, `statement`, and `parameters` as explicit keyword
136
+ arguments. `name` and `statement` must be string literals, preventing SQL construction through
137
+ f-strings or string concatenation.
138
+
139
+ ## Built-in rules
140
+
141
+ With the default `strict = true`, `CAT001` is advisory and the other 47 rules are enforced.
142
+ Individual rules cannot be disabled. Use `strict = false` before adoption to report every finding
143
+ as a warning.
144
+
145
+ | Group | Rules |
146
+ |---|---|
147
+ | Architecture | `ARCH000`-`002`, `BOUNDARY001`-`003`, `ENTRY001`, `SERVICE001`, `QUERY001`, `MODEL001`, `ADAPTER001`-`002`, `WIRING001`, `CONFIG001`, `DEPENDS001` |
148
+ | Runtime safety | `TIME001`, `ASYNC001`, `RUNTIME001`, `IMPORT001`, `IMPORT002`, `SIZE001`, `SEC001` |
149
+ | Database and transactions | `TX001`, `TX002`, `SESSION001`-`003`, `ORM001`, `ORM002`, `DB001`, `SQL001` |
150
+ | External calls | `HTTP001`, `LOG001`, `CAT001` |
151
+ | Data contracts | `DTO001`, `DTO002`, `SNAPSHOT001`, `SCHEMA001`-`003`, `API001`-`003`, `ENUM001`, `EXC001` |
152
+ | Test boundaries | `TEST001`, `TEST002` |
153
+ | Inline ignores | `IGNORE001` |
154
+
155
+ Each rule declares whether it applies to `prod`, `test`, `migration`, or `script` code. Missing
156
+ roles, dependency cycles, import placement, file size, dynamic execution, async safety, and
157
+ security access are checked in every zone. API, DTO, database, and service-boundary rules apply to
158
+ production code.
159
+
160
+ An unregistered call that might have an external effect cannot be proven unsafe, so it is reported
161
+ as a `CAT001` warning. After classifying the call, add it to the project effect catalog for precise
162
+ enforcement.
163
+
164
+ ## Development
165
+
166
+ ```bash
167
+ bash scripts/test.sh
168
+ bash scripts/test.sh --only tests/unit/policy/test_builtin_rules.py -x
169
+ ```
170
+
171
+ The full check runs the repository's own policy rules, Ruff, mypy strict, Pyright strict, pytest
172
+ with at least 90% branch coverage, package builds, and an isolated wheel installation.
173
+
174
+ See [`docs/README.md`](docs/README.md) for the current design documents.
@@ -0,0 +1,88 @@
1
+ taut/__init__.py,sha256=HnAKS-Y86m4B9dLgE-zEJW_T_BCWzrGPUqt20TKNLbs,117
2
+ taut/cli.py,sha256=eIHnaVokfWeD5XbmIqUMMvzrFRkC9xbhA-74fb_f38I,11088
3
+ taut/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
+ taut/analysis/__init__.py,sha256=vMBCVVPvRxyWpXh6eZfI96zHlBhT0FKOoKtYGP60ZyU,84
5
+ taut/analysis/contracts.py,sha256=meMv6Y9hdOY4tJUYyx_tnDrXMjKGv069TeZS747RwjY,3253
6
+ taut/analysis/module_analysis.py,sha256=g1wBRZdfdHxkHN7qKxbMQJA_cXHfZ5YYVCohC4fSk2o,1042
7
+ taut/analysis/project_analyzer.py,sha256=LoOJkk9lmbzcepWJnIe6hdWEqcvg4DPm9SL_DibFnrA,2959
8
+ taut/analysis/project_index.py,sha256=IlWHSw-MIv7520YCiuIBVGO9CrbDZ9c36iu5piJaGeo,4572
9
+ taut/analysis/semantic_model.py,sha256=PTHs_--Z98-bxU-WJBVpAL5IEcuVLFO8vZ9W-d9tjZk,2280
10
+ taut/analysis/python/__init__.py,sha256=gl5VU3-h8qWbCKoNnPJjQaU1dj6mddh751WFiEwB0wY,67
11
+ taut/analysis/python/adapter.py,sha256=nrnXVa1POjK6131ePDEOErMb3DngUo0DdMJ37W2UUW8,16558
12
+ taut/analysis/python/expression_summary.py,sha256=UttgAXYppatHOq8d9jHbUeJMnKLT4JgFGalkMzAqzao,6042
13
+ taut/analysis/python/failed_analysis.py,sha256=RPLjne6XBKGta6zfKUeQtMWx5TbIReUXtNExC15joJc,1217
14
+ taut/analysis/python/language_adapter.py,sha256=maaNu8E3uObjMWLS6O8k4O8YFOl6MJX_dyz_ArBDP20,3385
15
+ taut/analysis/python/symbol_resolver.py,sha256=0wontLEU9JHB34qpVf92MS6hJ2B0Jwn64wvD7FqQkpY,8367
16
+ taut/configuration/__init__.py,sha256=npHXSsBE-UiKtvuIDxHAhYykF119d6kRvBtAt78tqJU,96
17
+ taut/configuration/catalog.py,sha256=DbNTmPuD4_xGQ7wxytLaQu4gt1VhEQXMQIleDsVlnxI,3081
18
+ taut/configuration/effective_policy.py,sha256=PJcI99h3bmHavJRYIg3MuNk_bDIQMez5NdAdG2xA0Sg,18742
19
+ taut/configuration/manifest.py,sha256=GFENSLIQ7NIZ7_S0Xm-kJMvivJkLW3J742H3bZigLIE,3964
20
+ taut/configuration/model.py,sha256=QmfzAbOBmJq_xFlAm-jzDQfeB70a3M_RmvZ4U-ogOVU,1847
21
+ taut/configuration/rule_standard.py,sha256=RFlaMJsKbw2wtafpcVkK9aIzG4SpwDYREfkrw1H4d68,1293
22
+ taut/configuration/validation.py,sha256=bZxfxtK9wOuKBVIzE5GsLkJbJ0Y1S3egWKmfA7YXtGE,755
23
+ taut/domain/__init__.py,sha256=Ee4ognEKqbz7uhFLFi090lGtYW3WixnN9-T5lMfT0ec,102
24
+ taut/domain/diagnostics.py,sha256=vu0fMhV_C9dkT8G2QKgMK5f5fkh3Zuq4DAfQcwZ78ek,890
25
+ taut/domain/evaluations.py,sha256=kPJ9JuSc8NPOrukfaCWiHTazU9wtwO0BeB7yZ0rJCbA,2388
26
+ taut/domain/facts.py,sha256=zlnk7IaNnV_60WQPCVCwaXSLb5PqoBYPteOPucX7trE,11261
27
+ taut/domain/findings.py,sha256=DXmtqa2eV0QUD1Dv_R03v1thLDDh555R9zRTMCS9HD4,2166
28
+ taut/domain/frozen.py,sha256=l_L9QGpNZGe3rhGStmrcmA7YBA-ydkM2YboieB1HNyU,1517
29
+ taut/domain/ids.py,sha256=WyT3Snhm44RBIvp0TlFRHsK9riTBGPe2x_Vn-V99vQ4,2081
30
+ taut/domain/ignores.py,sha256=ETKQufcK1Gx96oiiPJbwNBYCjKJ-IP6vdn1NqocTaDY,545
31
+ taut/domain/issues.py,sha256=KYYb0qcvQsIeMPSUApszJC9nktyCOcd-v-wnZfAPhR4,1004
32
+ taut/domain/location.py,sha256=81N0kCUGFvfL4T5AQSJHJsXLdYy3j3mjzOJgZg5b5-E,2744
33
+ taut/domain/provenance.py,sha256=tMwIijPJQxTVJX_ovxdHF9al-N0pUsCfW4j6u55VllI,615
34
+ taut/domain/reports.py,sha256=K_Y-lkgraUJqcJcNB_AGZenKUmk-_rHaSzGFO-MzooI,2478
35
+ taut/domain/snapshot.py,sha256=D14UnwFigZRb33zzdZ14nog-M7bfCZF8nycAIXxAo40,2524
36
+ taut/finding_processing/__init__.py,sha256=zzAw-zCHHBIdoSRjBKP9-kWQq863vRAdd53fi183PGY,85
37
+ taut/finding_processing/finding_processor.py,sha256=zktgKM5YkKIfzE35SY-ub1luzpxTufA4XI2t7ctY7fs,11040
38
+ taut/finding_processing/report_builder.py,sha256=R5lK5z20bNMSLJP34K5E8PJ077jM_xF_dp3qJXIQDNM,1724
39
+ taut/loading/__init__.py,sha256=3VmDcvGO8Lg23_qHTvi9pu7YFghN3weLLPyVg6HREOc,92
40
+ taut/loading/boundary_extension_schema.py,sha256=Sh0Sin23Jt3soO9jvFgHaTcbXKgfYBHn4E6HPqrBbss,1260
41
+ taut/loading/builtin_catalog.py,sha256=G3akQ-j0XNkW9HJwP2MO4j8UONW5Kc6-FxpQVbzZgM8,2178
42
+ taut/loading/code_conventions.py,sha256=x6ZjcljlabiGsA6u5oTmPL7n6R3rl_MBIgRyVzUKkcg,5398
43
+ taut/loading/config_loader.py,sha256=AzPtoZOfOrNLKc7xxVRoCDRJZENQvkqZ5rs5pRiVlkk,20067
44
+ taut/loading/configuration_document.py,sha256=2MrtZZtL92vfg5gHzPCRXfjz39LCZtBVn0nG4g0EGPg,7743
45
+ taut/loading/default_configuration.py,sha256=HnCwTUMBiOhtAQnBqLaP7bCYOQ3qZHQ-vwN54Y0rReM,2271
46
+ taut/loading/errors.py,sha256=0pW0tXm7pMwyFG8iFMa5BQfEgAYDAJsgw_RRBs8lTsE,46
47
+ taut/loading/inline_ignores.py,sha256=zZto8e4ZlPGVwdQIDDbRVxUrQqTKP8LPEnaNhwPlU2k,3066
48
+ taut/loading/source_discovery.py,sha256=o8EmqhDrP97NwkL7Z--Q-Vsv4hQJal6QtfmkwwsNS0I,6002
49
+ taut/policy/__init__.py,sha256=B9COZ5C-YzbssYbmCU9kUf-e9aEYle_xs68fCwuYO7k,89
50
+ taut/policy/context.py,sha256=QjS4XCfHkLyOj3nhZdommCFcghA0ak6S4UVQUuG2f_c,1436
51
+ taut/policy/decision_digest.py,sha256=-96HZRFjsop8giUeHxx23-HdzCZddSsq7Czv5qCAZ1g,999
52
+ taut/policy/engine.py,sha256=DFaFXGt2BtNWEckbZ3CrRJfqSBMXJMmvbbQ8bE1Vde4,7465
53
+ taut/policy/indexes.py,sha256=jb0IJUeELEorWuCz22TfYMJgAykRM0-qq5S769o-YP8,3525
54
+ taut/policy/registry.py,sha256=KvDdrtYoWe6cjtqrL_x5yqrcO9CJUPmi3WA4pXrHmso,969
55
+ taut/policy/rule.py,sha256=i4Cs8ol4OkdIINUv5zulvA0J23kPqUmOs7OKLJH2oqY,2591
56
+ taut/policy/scheduler.py,sha256=iqZ6aHthR2XkEM7Me7jddruH5o9uFU9yi6eRHF49wnQ,2179
57
+ taut/policy/rules/__init__.py,sha256=QJxHOyfZ4QsSCb0b5RjRhzvi0ZfVgbm37wCCJ7EgsbM,4019
58
+ taut/policy/rules/api_contracts.py,sha256=_yLrkGs3WIh9URw99-jUTUegLjRxIWCwyknMON1y4qA,19021
59
+ taut/policy/rules/architecture.py,sha256=kDxxGxSTPE52ddWeG0VZmyO8Kkj3TQ6NajS3nNJKAPc,6226
60
+ taut/policy/rules/async_safety.py,sha256=NEbAhEEgSRWnxidbjFbTVLh-PqGb8Yhingy63blrBxI,3291
61
+ taut/policy/rules/boundary.py,sha256=AcF9g-baLVgSCSuP_gq-E8pUaiYncJl_wzyq3hvayLQ,6193
62
+ taut/policy/rules/catalog_coverage.py,sha256=A257BUdlRbIVAk38QcWYOJHWbDB84oTG8dUqtrU-_RU,2902
63
+ taut/policy/rules/classification.py,sha256=Aa-pMVDeYZikoA0UP7e4jjulUlxcyTZRQWV0q_hALaE,2389
64
+ taut/policy/rules/construction_boundaries.py,sha256=_T03ZpAS0Q2SfKyKwPmd06TkaNr0Oz9wy0vjWGwfGEw,7803
65
+ taut/policy/rules/conventions.py,sha256=pWRjjSfaKwDL9nPJVbvcyeBior5a8ls2_K3zhjHyXrE,6150
66
+ taut/policy/rules/enums.py,sha256=_HjDtVqxyq3W2CD0dbm0RWg_JV3M2oZvligGI3eugwA,7790
67
+ taut/policy/rules/exceptions.py,sha256=8G4MVyhm0ci2LLrokzxxLsoZ_PFXLRdfuLQxhp7K1tU,9317
68
+ taut/policy/rules/external_calls.py,sha256=q28dQOXlfPrxnmcrXt_hsj-i-l4140DfWyjy14OfMGE,5583
69
+ taut/policy/rules/helpers.py,sha256=s1IsXcUUhEZJJuQLG8UqIIGixeWEeKQM6CPOdvua_8o,1386
70
+ taut/policy/rules/ignore_audit.py,sha256=KvZ5S2D6DwKxRR-b3lQrXYEY8ZVYPm0Vikl5P8sc8cY,1486
71
+ taut/policy/rules/layer_boundaries.py,sha256=skOAI9NIqm981OEZHdGb8ZUlO8T1VsT8qVSbn-dJNo8,17201
72
+ taut/policy/rules/model_shapes.py,sha256=oR4Svg4VEwqB-_ckk-5ExSkJrVTJ_TFa26wjTLbKNWU,13919
73
+ taut/policy/rules/persistence.py,sha256=Y7RE25YE6ZJKXfYObTa0FzLO4S2_StabYr1RO1hrP5E,14860
74
+ taut/policy/rules/responsibility_boundaries.py,sha256=jh_77h7-2DXyOreNHnZxNWcqxIVIn8y4Ce8GLxUsa1w,9009
75
+ taut/policy/rules/runtime_safety.py,sha256=q9VW24VnOApTPAyrC7c0xXZIF9n3Q8maGQzyVa7u-pU,6934
76
+ taut/policy/rules/security.py,sha256=VxcDU7qubZiF1b-tEWCSJ9vKwWHl-UPpg9rstDVsLBc,5245
77
+ taut/policy/rules/session.py,sha256=rD91qGaD1KXyM5z0PNK7ae9rJkLiRZEp1_WNVLNy0Ow,8614
78
+ taut/policy/rules/test_boundaries.py,sha256=QrX9s4c2Z35kXgj2tt3dlDqb_LEKm80XqwPKPc-Y6yY,4567
79
+ taut/policy/rules/time_access.py,sha256=UM-5qkBgVcLkMoZy8G8zdCqCM0u6ZxdancaKTGfk7_c,3810
80
+ taut/policy/rules/transaction.py,sha256=rkYfQOrssp38HWb4T_5LN3bQ2W5Y5AMVGOhMX21t9aQ,4794
81
+ taut/reporting/__init__.py,sha256=KZxYW1rOm_idC3mb9KDNPVLcxRvC7ZejFHrgEoxOqr0,82
82
+ taut/reporting/json.py,sha256=VisbIGAvy_0_V1IpYyeBVnct4Zouw4HKBP5uMftElhc,4446
83
+ taut/reporting/text.py,sha256=sTivtti4r9guuK4ZapsGdwkGQXB2jQFrJLmLKcNe2xA,7188
84
+ pytaut-0.1.0.dist-info/METADATA,sha256=A12m5V0Wp9bqVwPwS6HjCDV2dZDc1ReF0v-I4fEtiLk,6355
85
+ pytaut-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
86
+ pytaut-0.1.0.dist-info/entry_points.txt,sha256=XmqwEA_0kKek9F2umLrcVkSAGv5KDfNDkQgaVwnY_68,39
87
+ pytaut-0.1.0.dist-info/licenses/LICENSE,sha256=GGKppGXt0fZTXrHpv0D5k0wXwExUBY2uSSmYZyGzRrs,1067
88
+ pytaut-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ taut = taut.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TaeWoo Kim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
taut/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Deterministic policy checks for Python backend architecture."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ """Source analysis and immutable snapshot construction."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Protocol
6
+
7
+ from taut.domain.facts import ModuleFacts, SourceKind
8
+ from taut.domain.frozen import FrozenMap
9
+ from taut.domain.ids import ModuleId, SymbolId
10
+ from taut.domain.issues import EngineIssue
11
+ from taut.domain.location import ProjectPath
12
+
13
+
14
+ @dataclass(frozen=True, order=True)
15
+ class ProjectRoot:
16
+ value: Path
17
+
18
+ def __post_init__(self) -> None:
19
+ if not self.value.is_absolute():
20
+ raise ValueError("project root must be an absolute path")
21
+
22
+
23
+ @dataclass(frozen=True, order=True)
24
+ class LanguageSettings:
25
+ language: str = "python"
26
+ target_version: str = "3.12"
27
+
28
+
29
+ @dataclass(frozen=True, order=True)
30
+ class ContextManagerProvider:
31
+ symbol: SymbolId
32
+ item_type: SymbolId
33
+
34
+
35
+ @dataclass(frozen=True, order=True)
36
+ class ResolverSettings:
37
+ source_roots: tuple[ProjectPath, ...] = (ProjectPath("."),)
38
+ context_manager_providers: tuple[ContextManagerProvider, ...] = ()
39
+
40
+ def __post_init__(self) -> None:
41
+ symbols = tuple(provider.symbol for provider in self.context_manager_providers)
42
+ if len(symbols) != len(set(symbols)):
43
+ raise ValueError("context manager provider symbols must be unique")
44
+ if self.context_manager_providers != tuple(sorted(self.context_manager_providers)):
45
+ raise ValueError("context manager providers must be sorted")
46
+
47
+
48
+ @dataclass(frozen=True, order=True)
49
+ class SourceInput:
50
+ path: ProjectPath
51
+ module_id: ModuleId
52
+ kind: SourceKind
53
+ is_policy_target: bool
54
+ is_package: bool
55
+ content: str
56
+ content_hash: str
57
+
58
+ def __post_init__(self) -> None:
59
+ if len(self.content_hash) != 64:
60
+ raise ValueError("source content hash must be a sha256 digest")
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class AnalysisRequest:
65
+ project_root: ProjectRoot
66
+ sources: tuple[SourceInput, ...]
67
+ language: LanguageSettings
68
+ resolver: ResolverSettings
69
+ adapter_versions: FrozenMap[str, str]
70
+
71
+ def __post_init__(self) -> None:
72
+ identities = tuple(source.module_id for source in self.sources)
73
+ if len(identities) != len(set(identities)):
74
+ raise ValueError("analysis request contains duplicate module ids")
75
+ paths = tuple(source.path.value.casefold() for source in self.sources)
76
+ if len(paths) != len(set(paths)):
77
+ raise ValueError("analysis request contains duplicate or case-conflicting paths")
78
+ if self.sources != tuple(sorted(self.sources, key=lambda source: source.path.value)):
79
+ raise ValueError("analysis sources must be sorted by project path")
80
+
81
+
82
+ @dataclass(frozen=True, order=True)
83
+ class AdapterIdentity:
84
+ name: str
85
+ version: str
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class ModuleAnalysisResult:
90
+ facts: ModuleFacts
91
+ issues: tuple[EngineIssue, ...]
92
+
93
+
94
+ class LanguageAdapter(Protocol):
95
+ identity: AdapterIdentity
96
+
97
+ def analyze_module(
98
+ self,
99
+ source: SourceInput,
100
+ resolver: ResolverSettings | None = None,
101
+ ) -> ModuleAnalysisResult: ...
102
+
103
+ def analyze_modules(
104
+ self,
105
+ sources: tuple[SourceInput, ...],
106
+ resolver: ResolverSettings,
107
+ workers: int,
108
+ ) -> tuple[ModuleAnalysisResult, ...]: ...
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from taut.domain.facts import AnalysisStage
6
+
7
+ _NEXT_STAGE = {
8
+ AnalysisStage.DISCOVERED: AnalysisStage.PARSED,
9
+ AnalysisStage.PARSED: AnalysisStage.INDEXED,
10
+ AnalysisStage.INDEXED: AnalysisStage.RESOLVED,
11
+ AnalysisStage.RESOLVED: AnalysisStage.FACTS_READY,
12
+ }
13
+
14
+
15
+ @dataclass
16
+ class ModuleAnalysis:
17
+ """Mutable analysis lifecycle. This value never leaves the analyzer."""
18
+
19
+ stage: AnalysisStage = AnalysisStage.DISCOVERED
20
+
21
+ def advance(self, stage: AnalysisStage) -> None:
22
+ if self.stage is AnalysisStage.FAILED:
23
+ raise ValueError("failed analysis cannot advance")
24
+ if _NEXT_STAGE.get(self.stage) is not stage:
25
+ raise ValueError(f"cannot move analysis from {self.stage.value} to {stage.value}")
26
+ self.stage = stage
27
+
28
+ def fail(self) -> None:
29
+ if self.stage is AnalysisStage.FACTS_READY:
30
+ raise ValueError("completed analysis cannot be marked failed")
31
+ self.stage = AnalysisStage.FAILED
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+
6
+ from taut.analysis.contracts import AnalysisRequest, LanguageAdapter
7
+ from taut.analysis.project_index import build_project_index
8
+ from taut.domain.facts import CompletenessState, ModuleFacts
9
+ from taut.domain.frozen import FrozenMap
10
+ from taut.domain.ids import SnapshotId
11
+ from taut.domain.issues import EngineIssue
12
+ from taut.domain.snapshot import (
13
+ AnalysisCoverage,
14
+ AnalysisInputDigest,
15
+ AnalysisSnapshot,
16
+ )
17
+
18
+
19
+ class ProjectAnalyzer:
20
+ def __init__(self, adapter: LanguageAdapter) -> None:
21
+ self._adapter = adapter
22
+
23
+ def analyze(self, request: AnalysisRequest, *, workers: int = 1) -> AnalysisSnapshot:
24
+ if workers < 1:
25
+ raise ValueError("analysis workers must be positive")
26
+ modules: list[ModuleFacts] = []
27
+ issues: list[EngineIssue] = []
28
+ results = self._adapter.analyze_modules(request.sources, request.resolver, workers)
29
+ for result in results:
30
+ modules.append(result.facts)
31
+ issues.extend(result.issues)
32
+
33
+ module_map = FrozenMap((facts.module.id, facts) for facts in modules)
34
+ project = build_project_index(modules)
35
+ states = tuple(facts.completeness.state for facts in modules)
36
+ coverage = AnalysisCoverage(
37
+ requested_sources=len(request.sources),
38
+ complete_modules=states.count(CompletenessState.COMPLETE),
39
+ partial_modules=states.count(CompletenessState.PARTIAL),
40
+ failed_modules=states.count(CompletenessState.FAILED),
41
+ )
42
+ digest = _analysis_digest(request)
43
+ return AnalysisSnapshot(
44
+ id=SnapshotId(digest),
45
+ inputs=AnalysisInputDigest(digest),
46
+ modules=module_map,
47
+ project=project,
48
+ capabilities=FrozenMap(),
49
+ coverage=coverage,
50
+ issues=tuple(issues),
51
+ )
52
+
53
+
54
+ def _analysis_digest(request: AnalysisRequest) -> str:
55
+ payload = {
56
+ "sources": [
57
+ {
58
+ "path": source.path.value,
59
+ "module": source.module_id.value,
60
+ "kind": source.kind.value,
61
+ "target": source.is_policy_target,
62
+ "hash": source.content_hash,
63
+ }
64
+ for source in request.sources
65
+ ],
66
+ "language": {
67
+ "name": request.language.language,
68
+ "target_version": request.language.target_version,
69
+ },
70
+ "resolver": {
71
+ "source_roots": [root.value for root in request.resolver.source_roots],
72
+ "context_manager_providers": [
73
+ (provider.symbol.value, provider.item_type.value)
74
+ for provider in request.resolver.context_manager_providers
75
+ ],
76
+ },
77
+ "adapters": list(request.adapter_versions.items()),
78
+ }
79
+ encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
80
+ return hashlib.sha256(encoded).hexdigest()
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from taut.domain.facts import (
6
+ ImportCycle,
7
+ ModuleFacts,
8
+ ProjectIndex,
9
+ UnresolvedImport,
10
+ )
11
+ from taut.domain.frozen import FrozenMap
12
+ from taut.domain.ids import ModuleId
13
+
14
+
15
+ def build_project_index(modules: Iterable[ModuleFacts]) -> ProjectIndex:
16
+ module_list = tuple(sorted(modules, key=lambda facts: facts.module.id))
17
+ module_ids = frozenset(facts.module.id for facts in module_list)
18
+ modules_by_name = {module_id.value: module_id for module_id in module_ids}
19
+ internal_roots = frozenset(module_id.value.split(".")[0] for module_id in module_ids)
20
+ imports: dict[ModuleId, set[ModuleId]] = {module_id: set() for module_id in module_ids}
21
+ unresolved: list[UnresolvedImport] = []
22
+
23
+ for facts in module_list:
24
+ for import_fact in facts.imports:
25
+ target = _resolve_internal_import(
26
+ import_fact.imported_name,
27
+ import_fact.imported_module_name,
28
+ modules_by_name,
29
+ )
30
+ if target is not None:
31
+ imports[facts.module.id].add(target)
32
+ continue
33
+ root = import_fact.imported_module_name.split(".")[0]
34
+ possibly_internal = import_fact.relative_level > 0 or root in internal_roots
35
+ if possibly_internal:
36
+ unresolved.append(
37
+ UnresolvedImport(
38
+ importer=facts.module.id,
39
+ written_name=import_fact.imported_name,
40
+ location=import_fact.location,
41
+ reason="프로젝트 내부 모듈로 연결하지 못했습니다.",
42
+ )
43
+ )
44
+
45
+ frozen_imports = FrozenMap(
46
+ (source, tuple(sorted(targets))) for source, targets in imports.items()
47
+ )
48
+ reverse: dict[ModuleId, set[ModuleId]] = {module_id: set() for module_id in module_ids}
49
+ for source, targets in imports.items():
50
+ for target in targets:
51
+ reverse[target].add(source)
52
+ frozen_reverse = FrozenMap(
53
+ (target, tuple(sorted(sources))) for target, sources in reverse.items()
54
+ )
55
+ cycles = _find_cycles(frozen_imports)
56
+ return ProjectIndex(
57
+ imports=frozen_imports,
58
+ imported_by=frozen_reverse,
59
+ unresolved_imports=tuple(
60
+ sorted(
61
+ unresolved,
62
+ key=lambda item: (
63
+ item.location.path.value,
64
+ item.location.start_line,
65
+ item.location.start_column,
66
+ item.written_name,
67
+ ),
68
+ )
69
+ ),
70
+ cycles=cycles,
71
+ )
72
+
73
+
74
+ def _resolve_internal_import(
75
+ imported_name: str,
76
+ imported_module_name: str,
77
+ modules_by_name: dict[str, ModuleId],
78
+ ) -> ModuleId | None:
79
+ candidates = (imported_name, imported_module_name)
80
+ for candidate in candidates:
81
+ current = candidate
82
+ while current:
83
+ module_id = modules_by_name.get(current)
84
+ if module_id is not None:
85
+ return module_id
86
+ current = current.rpartition(".")[0]
87
+ return None
88
+
89
+
90
+ def _find_cycles(
91
+ graph: FrozenMap[ModuleId, tuple[ModuleId, ...]],
92
+ ) -> tuple[ImportCycle, ...]:
93
+ index = 0
94
+ stack: list[ModuleId] = []
95
+ on_stack: set[ModuleId] = set()
96
+ indexes: dict[ModuleId, int] = {}
97
+ low_links: dict[ModuleId, int] = {}
98
+ components: list[tuple[ModuleId, ...]] = []
99
+
100
+ def visit(node: ModuleId) -> None:
101
+ nonlocal index
102
+ indexes[node] = index
103
+ low_links[node] = index
104
+ index += 1
105
+ stack.append(node)
106
+ on_stack.add(node)
107
+
108
+ for target in graph[node]:
109
+ if target not in indexes:
110
+ visit(target)
111
+ low_links[node] = min(low_links[node], low_links[target])
112
+ elif target in on_stack:
113
+ low_links[node] = min(low_links[node], indexes[target])
114
+
115
+ if low_links[node] != indexes[node]:
116
+ return
117
+ component: list[ModuleId] = []
118
+ while True:
119
+ member = stack.pop()
120
+ on_stack.remove(member)
121
+ component.append(member)
122
+ if member == node:
123
+ break
124
+ ordered = tuple(sorted(component))
125
+ if len(ordered) > 1 or (len(ordered) == 1 and ordered[0] in graph[ordered[0]]):
126
+ components.append(ordered)
127
+
128
+ for module_id in graph:
129
+ if module_id not in indexes:
130
+ visit(module_id)
131
+
132
+ return tuple(ImportCycle(component) for component in sorted(components))
@@ -0,0 +1,3 @@
1
+ """Python stdlib-ast language adapter."""
2
+
3
+ __all__: list[str] = []