foretop-lading 0.1.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 (52) hide show
  1. foretop_lading-0.1.0/LICENSE +202 -0
  2. foretop_lading-0.1.0/PKG-INFO +442 -0
  3. foretop_lading-0.1.0/README.md +426 -0
  4. foretop_lading-0.1.0/pyproject.toml +73 -0
  5. foretop_lading-0.1.0/pyproject.toml.orig +85 -0
  6. foretop_lading-0.1.0/src/keel/__init__.py +1 -0
  7. foretop_lading-0.1.0/src/keel/collect/__init__.py +0 -0
  8. foretop_lading-0.1.0/src/keel/collect/walk.py +95 -0
  9. foretop_lading-0.1.0/src/keel/finding.py +105 -0
  10. foretop_lading-0.1.0/src/keel/gate.py +151 -0
  11. foretop_lading-0.1.0/src/keel/git_ref.py +45 -0
  12. foretop_lading-0.1.0/src/keel/render/__init__.py +0 -0
  13. foretop_lading-0.1.0/src/keel/render/annotations.py +62 -0
  14. foretop_lading-0.1.0/src/keel/render/markdown.py +40 -0
  15. foretop_lading-0.1.0/src/keel/render/sarif.py +85 -0
  16. foretop_lading-0.1.0/src/keel/render/terminal.py +38 -0
  17. foretop_lading-0.1.0/src/keel/render/triage_json.py +15 -0
  18. foretop_lading-0.1.0/src/keel/report.py +97 -0
  19. foretop_lading-0.1.0/src/keel/run.py +66 -0
  20. foretop_lading-0.1.0/src/keel/triage.py +166 -0
  21. foretop_lading-0.1.0/src/lading/__init__.py +1 -0
  22. foretop_lading-0.1.0/src/lading/__main__.py +4 -0
  23. foretop_lading-0.1.0/src/lading/cli.py +303 -0
  24. foretop_lading-0.1.0/src/lading/curate.py +111 -0
  25. foretop_lading-0.1.0/src/lading/data/licences.yaml +555 -0
  26. foretop_lading-0.1.0/src/lading/data/model_overrides.yaml +56 -0
  27. foretop_lading-0.1.0/src/lading/data/package_overrides.yaml +677 -0
  28. foretop_lading-0.1.0/src/lading/dependency_scope.py +118 -0
  29. foretop_lading-0.1.0/src/lading/discover_adapters.py +63 -0
  30. foretop_lading-0.1.0/src/lading/discover_cyclonedx.py +79 -0
  31. foretop_lading-0.1.0/src/lading/discover_datasets.py +27 -0
  32. foretop_lading-0.1.0/src/lading/discover_deps.py +30 -0
  33. foretop_lading-0.1.0/src/lading/discover_models.py +25 -0
  34. foretop_lading-0.1.0/src/lading/findings.py +249 -0
  35. foretop_lading-0.1.0/src/lading/hf_cache.py +46 -0
  36. foretop_lading-0.1.0/src/lading/licences.py +91 -0
  37. foretop_lading-0.1.0/src/lading/model_overrides.py +46 -0
  38. foretop_lading-0.1.0/src/lading/models.py +48 -0
  39. foretop_lading-0.1.0/src/lading/obligations.py +44 -0
  40. foretop_lading-0.1.0/src/lading/package_overrides.py +53 -0
  41. foretop_lading-0.1.0/src/lading/policy.py +116 -0
  42. foretop_lading-0.1.0/src/lading/render/__init__.py +0 -0
  43. foretop_lading-0.1.0/src/lading/render/annotations.py +53 -0
  44. foretop_lading-0.1.0/src/lading/render/json_renderer.py +10 -0
  45. foretop_lading-0.1.0/src/lading/render/markdown.py +45 -0
  46. foretop_lading-0.1.0/src/lading/render/mermaid.py +51 -0
  47. foretop_lading-0.1.0/src/lading/render/row.py +9 -0
  48. foretop_lading-0.1.0/src/lading/render/sarif.py +12 -0
  49. foretop_lading-0.1.0/src/lading/render/terminal.py +67 -0
  50. foretop_lading-0.1.0/src/lading/resolve.py +96 -0
  51. foretop_lading-0.1.0/src/lading/source_scan.py +94 -0
  52. foretop_lading-0.1.0/src/lading/triage.py +151 -0
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 the foretop authors
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,442 @@
1
+ Metadata-Version: 2.4
2
+ Name: foretop-lading
3
+ Version: 0.1.0
4
+ Summary: Traces licence obligations across code dependencies and model weights as one dependency graph — resolves each artifact's licence via Hugging Face / PyPI and prints a flat, honest table (unknown rather than guessed).
5
+ License-Expression: Apache-2.0
6
+ License-File: LICENSE
7
+ Requires-Dist: typer>=0.15
8
+ Requires-Dist: rich>=13.9
9
+ Requires-Dist: httpx>=0.28
10
+ Requires-Dist: pydantic>=2.9
11
+ Requires-Dist: pyyaml>=6.0
12
+ Requires-Dist: pathspec>=0.12
13
+ Requires-Dist: huggingface-hub>=1.28
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+
17
+ # lading
18
+
19
+ > Traces licence obligations across code, model weights, datasets and adapters as one
20
+ > dependency graph, and blocks only the policies the team explicitly chose.
21
+
22
+ Full product spec: `../../specs/lading.md`. This file tracks build status; the spec is the
23
+ source of truth for the eventual full scope.
24
+
25
+ ## Status
26
+
27
+ **Slice 1 (Session 21): discover Python deps + model IDs in code, resolve via HF/PyPI, print
28
+ a flat obligations table.**
29
+
30
+ `lading` is `specs/lading.md`'s **Shape A — OSS CLI + CI check** product (§0: *"Ship the free
31
+ CLI publicly during Phase 0... The three-paid-pilot gate applies to the hosted tier only."*) —
32
+ same shape ebb/telltale/charter already shipped, so unlike undertow there's no `PILOT_STATUS`
33
+ tension to resolve first.
34
+
35
+ ```
36
+ uv run --package foretop-lading lading scan <path> [--format table|json]
37
+ ```
38
+
39
+ **Decisions locked in from `specs/lading.md` §1's checkpoint**, all as stated defaults —
40
+ `DEC-PAY`/`DEC-AUTH` are hosted-tier-only and untouched until `PILOT_STATUS >= 3`; `DEC-LLM`
41
+ matches what this session already established for undertow (Anthropic API, not used by Slice 1
42
+ itself — decomposition is Slice 2's job); `DEC-01`/`DEC-02`/`DEC-03`/`DEC-04`/`DEC-05`/`DEC-06`
43
+ had no cost/vendor tradeoff the way undertow's `DEC-LLM` did, so none were re-litigated
44
+ interactively — same "defaults locked in, flagged transparently" pattern charter's own Session
45
+ 15 used.
46
+
47
+ **`discover_deps.py`** — parses `uv.lock`'s `[[package]]` entries via stdlib `tomllib` (no new
48
+ dependency). Scoped to `uv.lock` only — `specs/lading.md`'s own FR-001 also names pip/poetry
49
+ lockfiles and Node, but Slice 1's milestone text ("discover Python deps + model IDs") doesn't
50
+ name them; a documented gap, not a silent one. Sanity-checked against this monorepo's own real
51
+ `uv.lock` (100+ real packages), not just a hand-built fixture.
52
+
53
+ **`discover_models.py`** — line-based regex scan of `.py` files (via `keel.collect.walk.
54
+ iter_files`, third real consumer after ebb and telltale) for a `{namespace}/{name}`-shaped
55
+ string literal appearing on the same line as a recognized loader call (`from_pretrained`,
56
+ `pipeline(...)`, `hf_hub_download`, `snapshot_download`) — the same "good enough, not
57
+ semantically perfect" bound telltale's PromQL/OTTL matching and charter's rules-only classifier
58
+ already use, chosen to avoid false positives on unrelated slash-shaped strings (file paths,
59
+ URLs) without a full AST parser. Evidence carries `file:line`.
60
+
61
+ **A known, deliberate scope gap, found while smoke-testing against real loader calls**:
62
+ namespace-less canonical Hugging Face model ids (`gpt2`, `bert-base-uncased`,
63
+ `distilbert-base-uncased` — pre-namespace models that predate HF's `org/name` convention) are
64
+ **not** discovered, since they don't contain a `/` and are indistinguishable from any other
65
+ bare string literal without one. Requiring the `/` is what keeps false-positive noise down for
66
+ every other string in a real codebase; broadening the match would need a curated allow-list of
67
+ real bare model names, which is exactly the kind of curated-KB problem Slice 2's `DEC-02`
68
+ override layer is for, not this slice's regex.
69
+
70
+ **`resolve.py`** — `resolve_model`/`resolve_package` against the real, live Hugging Face Hub
71
+ API (`GET https://huggingface.co/api/models/{id}`) and PyPI JSON API (`GET https://pypi.org/
72
+ pypi/{name}/json`) — both fetched and verified live this session, not from memory. HF's own
73
+ `cardData.license` field can be a **string** (confirmed against the real, live
74
+ `meta-llama/Llama-3.2-1B` — a non-SPDX slug, `"llama3.2"`, Meta's own community-license name)
75
+ or a **list of strings** (confirmed against a real dataset card) — both shapes handled.
76
+ PyPI's own `info.license` is free text, not SPDX-validated, and inconsistent across the
77
+ ecosystem in practice — a real run against this monorepo's own `uv.lock` resolved `httpx`,
78
+ `anthropic`, `certifi` and others correctly, and left plenty genuinely `unknown` (packages that
79
+ declare their licence only via a PyPI trove classifier, which this slice doesn't read — a real,
80
+ expected metadata-quality gap, matching `specs/lading.md` §11's own "HF metadata quality is the
81
+ weak link" risk note, mirrored here on the PyPI side). Any failure — malformed id, 404, network
82
+ error, or a response with no licence field at all — resolves to `Resolution(licence=None,
83
+ resolved=False)`, never guessed, per `specs/lading.md` §4's own accuracy strategy. In-memory
84
+ dedup only within one run; no persistent cache yet (`specs/lading.md` §6's 7-day cache-expiry
85
+ rule is real but not this slice's job — a documented gap).
86
+
87
+ Both resolvers use a plain `httpx.Client()`, not `keel.fetch.make_safe_client()` — HF and PyPI
88
+ are two fixed, trusted hosts reached through a regex-validated id, not arbitrary vendor URLs
89
+ the way undertow's catalogue is; the SSRF-via-redirect concern `make_safe_client` exists for
90
+ doesn't apply the same way here. The id/name is still validated against a strict shape regex
91
+ before ever being interpolated into a URL, as a lighter defense-in-depth measure.
92
+
93
+ **`cli.py` / `render.py`** — `lading scan` prints one flat table (artifact, kind, licence,
94
+ resolved?, source URL), Rich-rendered with the same `rich.markup.escape` discipline every other
95
+ terminal renderer in this suite uses (a model id or package name is attacker-influenceable
96
+ source text — the same real bug class charter's Session 15 found and fixed). `--format json`
97
+ too. The table's own footer states "obligations to review, not legal advice" on every run —
98
+ `specs/lading.md` DEC-05's own framing, not optional.
99
+
100
+ Decomposing licences into the full obligation enum (attribution, share-alike, field-of-use,
101
+ ...), the curated override KB, policy presets, datasets/adapters, local cache scanning, the
102
+ Mermaid graph, CycloneDX input, and the GitHub Action are all later slices per `specs/
103
+ lading.md` §8's own table — none of that is here yet.
104
+
105
+ **Verified for real, not just via mocks**: `uv run --package foretop-lading pytest apps/lading/
106
+ tests -q` (24 tests, including two live, unauthenticated network calls against the real HF and
107
+ PyPI APIs), `make check` (repo-wide: 636 tests, mypy strict, ruff) — all green. Ran
108
+ `lading scan` against a real scratch project containing this monorepo's own real `uv.lock` plus
109
+ real `transformers`-style loader calls for `meta-llama/Llama-3.2-1B`,
110
+ `cardiffnlp/twitter-roberta-base-sentiment-latest`, confirming real licences resolve
111
+ end-to-end against the live APIs (`llama3.2`, `cc-by-4.0`) in both table and `--format json`
112
+ output.
113
+
114
+ **Slice 2 (Session 22): obligation decomposition + curated KB + policy engine.**
115
+
116
+ ```
117
+ uv run --package foretop-lading lading scan <path> [--format table|json] [--policy NAME_OR_PATH]
118
+ ```
119
+
120
+ `specs/lading.md` §8's own milestone text: *"Obligation decomposition + curated KB for 30 top
121
+ models + policy presets."* Slice 1 only ever showed a bare licence string; `DEC-03` (locked in
122
+ Session 21, no cost/vendor tradeoff, not re-litigated) calls for decomposed obligations instead
123
+ of one verdict, because *"Llama's acceptable-use terms are not a copyleft question."*
124
+
125
+ **`obligations.py`** — `ObligationType`: `attribution`, `redistribution`, `acceptable_use`,
126
+ `field_of_use`, `share_alike`. Every `Obligation` carries a non-blank, verbatim `clause` —
127
+ `specs/lading.md` §9's own definition of done: *"No obligation is ever displayed without a
128
+ quoted clause and a working source link."* `SHARE_ALIKE` has no curated example yet — every
129
+ real licence family researched this session (Apache-2.0, MIT, Llama 3/3.1/3.2, Gemma,
130
+ CreativeML OpenRAIL-M, BigScience BLOOM RAIL, OPT-175B, DeepSeek, FLUX.1 [dev] Non-Commercial,
131
+ ChatGLM3) turns out to be genuinely permissive or RAIL-restriction-propagating, never true
132
+ copyleft — a documented gap, not an invented example.
133
+
134
+ **Curation is per licence *family*, not per model** (`licences.py` + `src/lading/data/
135
+ licences.yaml`) — 12 real families curated this session, grounded in licence text fetched
136
+ live (Apache-2.0's and CreativeML OpenRAIL-M's/BLOOM RAIL's raw text fetched directly;
137
+ Meta's and Google's community-license pages are JS-rendered, so their `clause` fields use the
138
+ fragments a fetch tool returned in direct quotation marks against the live page — flagged in
139
+ the YAML's own header comment as a starting point for review, not a final legal read, matching
140
+ `DEC-05`). Real, meaningful differences found between families researched side by side: Llama's
141
+ field-of-use gate is a 700M-monthly-active-users commercial threshold, OPT-175B's is
142
+ non-commercial-research-only, FLUX.1 [dev]'s is non-commercial-only, ChatGLM3's is
143
+ free-for-research/register-for-commercial — four genuinely different shapes a binary verdict
144
+ would have flattened into one. Gemma, confirmed against its own real text, has no MAU-style
145
+ threshold at all, unlike its Llama-shaped siblings.
146
+
147
+ **`model_overrides.py` + `data/model_overrides.yaml`** — `DEC-02`'s hybrid override layer: 6
148
+ real models whose Hugging Face `cardData.license` is missing or the generic `other` value,
149
+ each researched to its real licence this session (`openai/clip-vit-base-patch32` → MIT, via
150
+ OpenAI's own CLIP repo; `facebook/opt-125m` → OPT-175B License; `deepseek-ai/
151
+ deepseek-llm-7b-base` → the DeepSeek License Agreement; `black-forest-labs/FLUX.1-dev` → its
152
+ own Non-Commercial License; `THUDM/chatglm3-6b`/`zai-org/chatglm3-6b` → the ChatGLM3-6B
153
+ License, covering both the old and the renamed org id). `curate.py` checks this file *before*
154
+ ever calling the live HF API for a model — a curated override is trusted more than a live
155
+ lookup that would only report `other` anyway. `stabilityai/stable-diffusion-2-1` was
156
+ investigated and deliberately left uncurated: the real HF API returned a genuine 401 for this
157
+ specific repo's metadata endpoint in this environment (confirmed three ways — curl, Python's
158
+ `urllib`, and a fetch tool), so rather than guess its licence from memory it's left to resolve
159
+ honestly to `unknown`, exactly the behaviour `specs/lading.md` §4 asks for when a real lookup
160
+ fails.
161
+
162
+ **`policy.py`** — pure, total `evaluate(resolution, policy) -> PolicyResult`
163
+ (`specs/lading.md` §6: *"every (obligations, policy) pair produces a decision, never an
164
+ exception"*) plus `DEC-04`'s three presets (`permissive-only`, `no-copyleft`,
165
+ `no-field-of-use-restrictions`) and a YAML override (`load_policy`). An unresolved or
166
+ not-yet-curated licence defaults to `warn`, never `fail`, for every preset — `specs/lading.md`
167
+ §4: *"`unknown` is a policy-configurable outcome (default: warn, not fail)."* `--policy` is
168
+ opt-in; omitting it reproduces Slice 1's exact output and the exit code is always 0.
169
+ `permissive-only` correctly fails a real Llama-family model on its field-of-use obligation —
170
+ `specs/lading.md` §9's own definition-of-done line, verified against the live API (see below),
171
+ not just a synthetic test.
172
+
173
+ **Verified for real**: `uv run --package foretop-lading pytest apps/lading/tests -q` (70
174
+ tests), repo-wide `make check` (682 tests, mypy strict, ruff) — all green. Ran `lading scan
175
+ --policy permissive-only` against a real scratch project referencing
176
+ `meta-llama/Llama-3.2-1B` and `google-bert/bert-base-uncased`, plus a real `requests` dependency
177
+ — confirmed end-to-end against the live HF/PyPI APIs: `requests` (Apache-2.0) and
178
+ `bert-base-uncased` (apache-2.0) both `pass`, `Llama-3.2-1B` (`llama3.2`) correctly `fail`s on
179
+ its real field-of-use obligation, and the process exits 1. Also confirmed the wheel actually
180
+ bundles `data/*.yaml` (`uv build --wheel` + inspected the resulting zip) — the data files live
181
+ inside the package's own source tree the same way `apps/ebb/src/ebb/registries/retirements/
182
+ *.yaml` already does in production, not a new pattern.
183
+
184
+ **Slice 3a (Session 23): datasets, vendored adapters, local HF cache scanning.**
185
+
186
+ ```
187
+ uv run --package foretop-lading lading scan <path> [--format ...] [--policy ...] [--scan-hf-cache]
188
+ ```
189
+
190
+ `specs/lading.md` §8's Slice 3 milestone bundles four things: *"Datasets, adapters, local
191
+ cache scanning, Mermaid graph, CycloneDX input."* Split into this slice (the three discovery-
192
+ breadth additions, all plugging into the existing discover → curate → resolve → policy
193
+ pipeline) and a later Slice 3b (Mermaid + CycloneDX, output/interop concerns with no shared
194
+ code path) — the same "shrink to one coherent slice" call Session 20 made for undertow's own
195
+ Slice 3.
196
+
197
+ **New dependency, asked and approved this session**: `huggingface_hub` (real PyPI: 1.28.0,
198
+ Apache-2.0). `--scan-hf-cache` needs to parse the real `~/.cache/huggingface/hub` layout
199
+ (`models--org--name/snapshots/<rev>/...`, no-namespace repos, non-symlink Windows mode, `xet/`
200
+ chunk dirs that aren't repos at all — confirmed against Hugging Face's own real cache-docs,
201
+ fetched live) — rather than hand-roll a parser for edge cases HF already solved, `hf_cache.py`
202
+ wraps the official `scan_cache_dir()`. **Opt-in only** (`--scan-hf-cache`), matching charter's
203
+ own opt-in `--enumerate` convention — a bare `lading scan .` never silently reaches outside the
204
+ repo directory into a user's real, potentially tens-of-GB machine-wide cache.
205
+
206
+ **A real bug, found by live verification, not by theorizing**: `scan_cache_dir()` *raises*
207
+ `CacheNotFound` rather than returning an empty report when the cache directory doesn't exist
208
+ yet — the ordinary case for a fresh machine or CI runner that's never downloaded anything from
209
+ Hugging Face, not a rare edge case. This environment has no HF cache at all, and running the
210
+ real (unmocked) code against it crashed immediately. Fixed by catching `CacheNotFound` in
211
+ `hf_cache.py` and treating it as "nothing cached" — the same "a missing source is empty, never
212
+ an exception" rule every other discoverer in this suite already follows. A real, hermetic test
213
+ (`HF_HUB_CACHE` pointed at a guaranteed-nonexistent temp path, not relying on any given
214
+ machine's real cache state) locks this in.
215
+
216
+ **`discover_datasets.py`** — same line-based, loader-call-adjacent pattern as
217
+ `discover_models.py` (now factored into a shared `source_scan.py`, genuinely identical logic
218
+ between the two, not merely similar), matching real `load_dataset(...)` calls. Same documented
219
+ false-positive-bounding gap as models: a bare, namespace-less dataset id (`"glue"`, `"squad"`)
220
+ isn't discovered.
221
+
222
+ **`discover_adapters.py`** — walks the repo for a real, vendored `adapter_config.json`
223
+ (confirmed live this session against a real PEFT adapter repo,
224
+ `peft-internal-testing/tiny-OPTForCausalLM-lora`, that this file always carries
225
+ `base_model_name_or_path`). Emits both the adapter itself (`kind=adapter`, identifier = its
226
+ directory path — no HF id of its own until pushed) and its base model
227
+ (`kind=model`) as separate artifacts. An adapter id referenced directly from Python source
228
+ (`PeftModel.from_pretrained(base_model, "org/adapter-id")`) was already caught by
229
+ `discover_model_ids`'s own `from_pretrained` matching before this slice — confirmed live via
230
+ PEFT's own quicktour docs — so this only covers the genuinely new gap: a config file committed
231
+ to the repo with no matching source-code string nearby.
232
+
233
+ **New `ArtifactKind` members**: `DATASET`, `ADAPTER` — additive, matching `specs/lading.md`
234
+ §3's own domain model wording (`artifacts.kind: code|weights|dataset|adapter`) more closely;
235
+ the existing `PYTHON_PACKAGE`/`MODEL` members were kept as-is rather than renamed, to avoid
236
+ unnecessary churn to already-shipped code for a naming-only concern.
237
+
238
+ **3 new curated licence families** (`data/licences.yaml`, 15 total now) — the real Creative
239
+ Commons families datasets actually use, fetched live from CC's own legal code text: `cc-by-4.0`
240
+ (genuinely permissive, attribution only), `cc-by-sa-4.0` (this KB's **first real
241
+ `share_alike` example** — every family curated in Session 22 turned out to be permissive or
242
+ RAIL-restriction-propagating, never true copyleft), `cc-by-nc-4.0` (a real, distinct
243
+ `field_of_use` shape — non-commercial only, confirmed by `specs/lading.md` §9's own
244
+ definition-of-done example: *"a CC-BY-NC dataset"*). `Salesforce/wikitext`'s real, live
245
+ `cc-by-sa-3.0` (not `-4.0`, the only version curated) is a real, honest "not yet decomposed"
246
+ case, not a bug.
247
+
248
+ **Verified for real**: `uv run --package foretop-lading pytest apps/lading/tests -q` (93
249
+ tests), repo-wide `make check` (705 tests, mypy strict, ruff) — all green. Ran `lading scan`
250
+ against a real scratch project with a real `load_dataset("openai/gsm8k", "main")` call, a real
251
+ `from_pretrained("google-bert/bert-base-uncased")` call, and a real vendored
252
+ `adapters/my-lora/adapter_config.json` (real shape, live-fetched) — confirmed end-to-end
253
+ against the live HF API: the dataset resolved to `mit`, the model to `apache-2.0`, the adapter
254
+ correctly `unknown` (no HF id), and its base model (a real internal-testing repo with
255
+ genuinely no `cardData` at all, confirmed via a direct API check) correctly `unknown` too, not
256
+ a bug. `--scan-hf-cache` itself has **no real end-to-end run** in this session — this
257
+ environment has no local HF cache to scan (confirmed: `~/.cache/huggingface` doesn't exist
258
+ here) — so beyond the real `CacheNotFound` bug fix above, this path is mock-verified only; a
259
+ real run needs a machine with actual cached models/datasets, which is the user's own job to
260
+ confirm.
261
+
262
+ **Slice 3b (Session 24): Mermaid graph output + CycloneDX SBOM input.**
263
+
264
+ ```
265
+ uv run --package foretop-lading lading scan <path> --format mermaid [--policy ...]
266
+ uv run --package foretop-lading lading scan <path> --sbom bom.json [--format ...]
267
+ ```
268
+
269
+ The second half of `specs/lading.md` §8's Slice 3 milestone, split from Slice 3a since Mermaid
270
+ rendering and CycloneDX ingestion are output/interop concerns with no shared code path with
271
+ 3a's discovery-breadth work — the same "shrink to one coherent slice" call Session 20 made for
272
+ undertow's own Slice 3.
273
+
274
+ **`render_mermaid`** (`render.py`) — `DEC-06`: *"Mermaid in the PR comment — reviewable
275
+ in-place, and it is the screenshot people share."* Wired into the existing `--format` flag as
276
+ a third value (`table|json|mermaid`), matching `specs/lading.md` FR-005's own framing of
277
+ Mermaid as a sibling output format, not a separate flag. One root node per scan fans out to
278
+ one node per artifact, coloured by `PolicyResult.decision` (green/yellow/red) via `classDef`/
279
+ `class` when `--policy` was supplied — confirmed live against mermaid.js.org's own current
280
+ docs as the non-deprecated syntax (`flowchart TD`, not the legacy bare `graph` keyword). Node
281
+ *ids* are synthetic (`n0`, `n1`, ...), never an artifact's own identifier — a real HF id like
282
+ `meta-llama/Llama-3.2-1B` would break Mermaid's own node-id syntax (no slashes allowed); the
283
+ real identifier only ever appears inside a quoted, escaped label. `_escape_mermaid_label`
284
+ applies the same defense-in-depth principle as `rich.markup.escape` elsewhere in this suite,
285
+ for Mermaid's own entity-escape syntax instead (`#` → `#35;`, `"` → `#quot;`, `#` escaped
286
+ first so its own escaped output is never re-escaped) — an artifact identifier or licence
287
+ string is text this process didn't originate and is never trusted not to contain characters
288
+ that would otherwise break the diagram.
289
+
290
+ **`discover_cyclonedx.py`** — accepts an existing CycloneDX 1.6 JSON SBOM as *input* via
291
+ `--sbom PATH` (additive to, never a replacement for, this command's own discovery — a team
292
+ composing lading with an existing SCA tool's SBOM still gets lading's own Python/model
293
+ discovery for anything the SBOM didn't cover). Real component shape and both supported purl
294
+ types were fetched live this session, not guessed: `pkg:pypi/{name}@{version}` (no namespace —
295
+ confirmed against cyclonedx.org's own real component examples and package-url/purl-spec's own
296
+ `pypi-definition.json`) and `pkg:huggingface/{namespace}/{name}@{revision}` (confirmed real
297
+ against purl-spec's own `huggingface-definition.json` and its test fixtures — a real ML-BOM
298
+ extension exists in CycloneDX 1.6, `type: "machine-learning-model"`, so a modern SBOM tool can
299
+ plausibly emit HF-model components, not just code packages). Every other purl type (npm,
300
+ maven, docker layers, ...) and any component with no purl at all is skipped, a documented gap
301
+ — real SBOMs commonly carry ecosystems lading doesn't resolve.
302
+
303
+ **A deliberate scope call, not a half-built feature**: a CycloneDX component's own already-
304
+ declared `licenses` field isn't consulted this slice. A parsed component becomes one more
305
+ artifact identifier feeding the exact same live resolve → curate → policy pipeline every other
306
+ discoverer uses, rather than a special-cased skip-the-live-call path — reusing the SBOM's own
307
+ declared licence directly would need a `DiscoveredArtifact` schema change to carry a pre-known
308
+ licence, real future work, not this slice's job.
309
+
310
+ **Verified for real**: `uv run --package foretop-lading pytest apps/lading/tests -q` (107
311
+ tests), repo-wide `make check` (719 tests, mypy strict, ruff) — all green. Ran
312
+ `lading scan --format mermaid --policy permissive-only` against a real scratch project (a real
313
+ `requests` dependency, a real `meta-llama/Llama-3.2-1B` reference) end-to-end against the live
314
+ HF/PyPI APIs: valid `flowchart TD` output, `requests` (Apache-2.0) coloured `pass`,
315
+ `Llama-3.2-1B` (`llama3.2`) coloured `fail` on its real field-of-use obligation, exit code 1 —
316
+ by eye, syntactically sane Mermaid. `--sbom` has no live-network verification of its own to run
317
+ (the SBOM file is local input, not a live API) — its purl-parsing correctness is covered by
318
+ real-shaped fixtures built from the fetched CycloneDX/purl-spec examples above, and its
319
+ resolve/curate/policy behaviour once parsed is exercised end-to-end with mocked HTTP; stated
320
+ plainly as mock-verified for that half, not claimed as more.
321
+
322
+ **Slice 4 (Session 25): GitHub Action, PR comment, CI gate, public mirror.**
323
+
324
+ `specs/lading.md` §8's last CLI-side milestone: *"GitHub Action, PR comment, CI gate,
325
+ Marketplace."* Not a novel design this session — ebb, telltale, and charter (all Shape A)
326
+ already built exactly this shape, read directly from their own code rather than re-derived
327
+ from memory, and replicated here with lading's own domain (obligations/policy decisions
328
+ instead of MCP capability drift or coverage regressions).
329
+
330
+ **Two new renderers** (`render/markdown.py`, `render/annotations.py` — `--format` now
331
+ `table|json|mermaid|markdown|annotations`), and **`render.py` split into a `render/` package**
332
+ (`terminal.py`, `json_renderer.py`, `mermaid.py`, `markdown.py`, `annotations.py`, `row.py`)
333
+ now that lading has reached the same 5-format count ebb's and charter's own `render/` packages
334
+ already use — not done earlier, since 3 formats didn't yet warrant it. `row.py` holds the
335
+ shared `Row` type alias in its own leaf module: `models.py` can't define it directly without
336
+ importing `curate.py`/`policy.py`, which already import from `resolve.py`, which imports from
337
+ `models.py` — a circular import `row.py` avoids by living outside that chain.
338
+
339
+ **`render_markdown`** mirrors `apps/charter/src/charter/render/markdown.py`'s own role
340
+ exactly — a PR-comment-ready document (table + summary) — plus the rendered Mermaid diagram
341
+ embedded in a ` ```mermaid ` fence, reusing `render_mermaid` directly rather than as a second,
342
+ separate CI step. This is what actually makes the graph *the* reviewable, shareable artifact
343
+ DEC-06 asks for, right inside the comment.
344
+
345
+ **`render_annotations`** emits `::error file=...,line=...::` GitHub workflow-commands — the
346
+ same zero-extra-permission mechanism (no `security-events: write`, no Advanced Security
347
+ requirement, unlike a SARIF-upload step) every other product in this suite already chose, for
348
+ the same reason, verified live once already and not re-verified a fourth time. One real
349
+ difference from charter's/telltale's own annotations: lading's `DiscoveredArtifact.evidence`
350
+ is a single string (Slice 1's own design), not a typed `(file, line)` pair, so a small
351
+ `_location` helper recovers the line number from a trailing `:digits` (the shape source-code
352
+ discovery produces) and falls back to line 1 for evidence with none (`uv.lock`, a CycloneDX
353
+ purl, `"hf cache: ..."`). Also unlike charter's/telltale's own drift-only annotations, *every*
354
+ current `fail` is annotated — lading has no `--base`/merge-base axis this CLI tracks yet, so
355
+ there's no "already existed" state to exclude.
356
+
357
+ **`apps/lading/action.yml`** — steps mirror charter's own exactly: scan with `--format
358
+ annotations` → render `--format markdown` for the sticky PR comment → upsert by marker
359
+ (`apps/lading/scripts/upsert_pr_comment.sh`, a byte-identical copy of the generic root-level
360
+ script) → fail on the captured exit code. `policy` defaults to `"permissive-only"` here, unlike
361
+ the bare CLI's own opt-in `--policy` — a CI Action whose whole purpose is being a gate can't
362
+ default to a silent no-op the way the general-purpose CLI reasonably can.
363
+
364
+ **Public mirror, matching `docs/adr/0002-ebb-public-mirror.md` and every predecessor's own
365
+ boundary exactly**: `.github/workflows/lading-{ci,release}.yml` (tag prefix `lading-v*`),
366
+ `scripts/mirror-workflows/lading-{ci,release}.yml`, and `scripts/sync-lading-mirror.sh` were
367
+ added. **Run for real against a scratch directory this session** (never against a real public
368
+ repo — same explicit warning charter's own script carries): first confirmed it reproduced the
369
+ same known packaging gap every predecessor's mirror had — `uv lock` failing because
370
+ `keel.collect.walk` only resolves inside this monorepo's own workspace — then, in a later
371
+ session, that gap was fixed for real across all four mirrors (ebb, telltale, charter, lading):
372
+ `scripts/sync-lading-mirror.sh` now vendors `keel.collect.walk` verbatim into the mirror at
373
+ sync time, and the mirror's own `pyproject.toml` (`scripts/mirror-pyproject/lading.toml`) no
374
+ longer depends on `foretop-keel`. See `packages/keel/README.md`'s own "Packaging note" for the
375
+ full mechanism. Re-verified for real after the fix: `ruff`/`mypy`/`pytest` (118 tests) all
376
+ green standalone, a real wheel built, and the built wheel cold-installed into a fresh venv with
377
+ `lading --help` actually running.
378
+
379
+ **Still the user's job, not this session's** (same split every predecessor used): creating the
380
+ `foretop-dev/lading` repo, actually running `sync-lading-mirror.sh` against it, creating the
381
+ `foretop-lading` PyPI project and configuring its trusted publisher (owner `foretop-dev`,
382
+ repository `lading`, workflow `release.yml`, environment `pypi`), cutting a `lading-v0.1.0`
383
+ release, and GitHub Marketplace submission — `docs/adr/0002` names Marketplace publishing
384
+ explicitly as *"a public launch action, deliberately left to the user."*
385
+
386
+ **Explicit non-goal this slice**: SARIF. Not named in this milestone's own wording (unlike
387
+ charter, where it arrived in an earlier slice) — same "match the milestone table's own words"
388
+ discipline used for every scope call this session.
389
+
390
+ **Verified for real**: `uv run --package foretop-lading pytest apps/lading/tests -q` (118
391
+ tests), repo-wide `make check` (730 tests, mypy strict, ruff) — all green; both new workflow
392
+ YAML files and `action.yml` parse as valid YAML. Ran `lading scan --format annotations
393
+ --policy permissive-only` and `--format markdown --policy permissive-only` against a real
394
+ scratch project (real `requests` + real `meta-llama/Llama-3.2-1B`) end-to-end against the live
395
+ HF/PyPI APIs: the annotation correctly points at `load.py:2` (the real line the
396
+ `from_pretrained` call is on) with the real field-of-use/acceptable-use reasons in its
397
+ message; the markdown output contains the correct table, summary, and a valid embedded
398
+ `flowchart TD` diagram.
399
+
400
+ **Session 28: `--format json` repurposed to emit the shared `Finding` schema.**
401
+
402
+ `SUITE_ARCHITECTURE.md` §3's keystone extraction (`packages/keel/src/keel/finding.py`,
403
+ Session 26) reached lading last of the three remaining adapters (telltale Session 26, charter
404
+ Session 27). `src/lading/findings.py`'s `to_findings()` maps a `Row`
405
+ (`DiscoveredArtifact`/`CuratedResolution`/`PolicyResult | None`) onto `Finding`, using
406
+ §3.1's own identity rule for lading — "artifact canonical id + obligation code" — as
407
+ `(artifact.kind, artifact.identifier, obligation.kind.value | None)`. Two finding kinds per
408
+ artifact, the same split charter's adapter used for (server, tool): one artifact-resolution
409
+ finding (`unknown` if unresolved or resolved-but-undecomposed, `clear` once decomposed,
410
+ regardless of obligation count) and one per `(artifact, obligation)` (`break` only when a
411
+ supplied `--policy` blocks that specific obligation; `clear` otherwise, including when no
412
+ `--policy` was given at all — judgment only happens when a policy exists to judge against,
413
+ mirroring charter's own "no `--base` ⇒ nothing is `break`" precedent).
414
+
415
+ **A real, breaking change, made deliberately, not by accident**: unlike telltale/charter (where
416
+ `json` didn't exist before their own adapter slice), lading already had a documented
417
+ `--format json` with its own row shape (identifier/kind/licence/obligations/decision). Asked
418
+ directly rather than assumed — the choice was to repurpose `--format json` in place to emit
419
+ the shared envelope, matching all three other adapters exactly, rather than add a second
420
+ format and leave lading as the one product where `json` doesn't mean `Finding`.
421
+ `PILOT_STATUS = 0` (nothing published/installed yet) made this the cheap side to choose: the
422
+ only real consumers were this repo's own tests, updated in the same commit.
423
+
424
+ **Evidence is genuinely heterogeneous** across lading's five discoverers — an absolute
425
+ lockfile path, a relative `path:line`, a bare purl, an `"hf cache: <path>"` descriptor — unlike
426
+ ebb/telltale/charter, which all have a real `source_file`/`source_line` pair to anchor on.
427
+ `_artifact_evidence()` parses defensively: strip a trailing `:<line>` if present, hash the real
428
+ file's bytes if the remaining text resolves to one, and otherwise fall back to hashing the
429
+ evidence string itself — still a real, deterministic, non-fabricated hash, just of the
430
+ descriptor text instead of file content when there is no file to hash.
431
+
432
+ **`rule_version` for an obligation finding** is the owning `LicenceEntry.verified_at` date
433
+ (ISO format), not a fabricated counter — `licences.yaml` has no taxonomy-wide version field the
434
+ way charter's `capability_taxonomy.yaml` does, and the per-entry verification date is a more
435
+ honest signal anyway.
436
+
437
+ Verified the same way Sessions 26/27 were: `lading scan --format json` against a real scratch
438
+ project (mocked HF/PyPI, same fixtures `test_cli.py` already builds) diffed key-for-key against
439
+ a fresh `ebb scan --format json` — identical envelope, four products now. `--policy
440
+ permissive-only` against the real `meta-llama/Llama-3.2-1B` field-of-use obligation produced a
441
+ `break` finding; the identical scan with no `--policy` produced `clear` for the same
442
+ obligation, identity unchanged across both runs.