throughline 4.2.0__tar.gz → 4.4.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.
- {throughline-4.2.0/src/throughline.egg-info → throughline-4.4.0}/PKG-INFO +17 -4
- {throughline-4.2.0 → throughline-4.4.0}/README.md +16 -3
- {throughline-4.2.0 → throughline-4.4.0}/pyproject.toml +1 -1
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/apply.py +136 -11
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/brief.py +60 -15
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/cli.py +42 -2
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/composition.py +1 -1
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/dump.py +13 -1
- throughline-4.4.0/src/throughline/inherit.py +242 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/items.py +37 -1
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/model.py +36 -2
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/parser.py +11 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/schema.py +35 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/schema_ops.py +79 -9
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/sources.py +36 -2
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/storage.py +204 -10
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/tomledit.py +7 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/union.py +2 -2
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/validate.py +24 -3
- {throughline-4.2.0 → throughline-4.4.0/src/throughline.egg-info}/PKG-INFO +17 -4
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline.egg-info/SOURCES.txt +4 -0
- throughline-4.4.0/tests/test_apply_once.py +197 -0
- throughline-4.4.0/tests/test_attribute_kinds.py +114 -0
- throughline-4.4.0/tests/test_schema_source.py +359 -0
- {throughline-4.2.0 → throughline-4.4.0}/LICENSE +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/NOTICE +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/setup.cfg +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/__init__.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/collision.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/diagrams.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/filters.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/fingerprint.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/graph.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/grounding.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/identity.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/inject.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/links.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/ratification.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/resolvers.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/seam.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/signatures.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/signed_links.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/uid.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/version.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/views.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline/worklist.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline.egg-info/dependency_links.txt +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline.egg-info/entry_points.txt +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline.egg-info/requires.txt +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/src/throughline.egg-info/top_level.txt +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_amend.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_apply.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_baseline.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_clarify.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_deprecated_names.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_diagram_cli.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_doctor.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_engine.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_filters.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_flag.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_library_api.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_link_guard.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_migrate_withdrawn.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_normative_by_type.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_own_tree.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_ratification_change.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_ratification_withdrawal.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_ratified_content.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_ratified_deletion.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_ratifier_correction.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_ratify_several.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_ratify_status.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_ratify_transition_refused_first.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_register_type.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_resolve_collision.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_resolve_cross_graph.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_schema_ops.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_signatures.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_signed_links.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_spec_lists.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_tombstone_record.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_unset_attrs.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_worklist.py +0 -0
- {throughline-4.2.0 → throughline-4.4.0}/tests/test_yaml_loader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: throughline
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: A Git-native requirements management tool with a scope-avalanche grounding layer: permanent UIDs, one file per item, typed links, suspect detection, and a CI-gating check.
|
|
5
5
|
Author-email: Henry J Grech-Cini <henry.grechcini@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -36,10 +36,10 @@ version control; a `check` command validates the whole graph and gates CI.
|
|
|
36
36
|
|
|
37
37
|
**Dogfooded:** throughline's own spec is itself a throughline project —
|
|
38
38
|
<!-- tl:count type == 'system_requirement' -->
|
|
39
|
-
|
|
39
|
+
248
|
|
40
40
|
<!-- tl:end --> system requirements,
|
|
41
41
|
<!-- tl:count type == 'user_requirement' -->
|
|
42
|
-
|
|
42
|
+
39
|
|
43
43
|
<!-- tl:end --> user requirements, and
|
|
44
44
|
<!-- tl:count type == 'nfr' -->
|
|
45
45
|
21
|
|
@@ -164,7 +164,7 @@ detect items that "violate" a non-goal — that judgement stays with a human.
|
|
|
164
164
|
```
|
|
165
165
|
tl init [--name NAME] # scaffold a project
|
|
166
166
|
tl register new <PREFIX> <dir> [--parent P] # add a register
|
|
167
|
-
tl new <PREFIX> [--uid U] [--type T] [--ground UID] # allocate + create (grounded at birth)
|
|
167
|
+
tl new <PREFIX> [--uid U] [--type T] [--ground UID] [--key UUID4] # allocate + create (grounded at birth); a held key creates nothing
|
|
168
168
|
tl link <SRC> <DST> --type <kind> [--stamp] # add a typed link
|
|
169
169
|
tl delete <UID> --reason "…" # propose a deletion: a tombstone a human ratifies (never erased)
|
|
170
170
|
tl restore <UID> # decline a proposed deletion
|
|
@@ -206,6 +206,7 @@ Upward and downward coverage are independent and both matter:
|
|
|
206
206
|
| `uid-filename` | an item's file is not named `<UID>.yml` (git could not see it collide) |
|
|
207
207
|
| `tombstone-deleted` | a retired UID's tombstone was erased (bad merge / stray `git rm`) |
|
|
208
208
|
| `schema` | missing required attr or out-of-enum value |
|
|
209
|
+
| `attribute-kind` | an attribute's value is not of its declared kind (`int`, `float`, `bool`, `date` as YYYY-MM-DD, `string`, `text`) |
|
|
209
210
|
| `suspect-link` | target changed since the link was last confirmed |
|
|
210
211
|
| `unreviewed` | item content changed since last review |
|
|
211
212
|
| `unratified` | AI-origin item still `proposed` |
|
|
@@ -241,6 +242,18 @@ reports every finding in `namespace:UID` vocabulary. `query`, `dump`, `docs`,
|
|
|
241
242
|
answer over the union too; everything else reads your graph alone, and a project
|
|
242
243
|
that declares no sources is untouched by any of this.
|
|
243
244
|
|
|
245
|
+
A graph whose own items follow a shared model — every deck in a series, every plan
|
|
246
|
+
written to one standard — can take its schema from a source instead of copying it.
|
|
247
|
+
Mark that one source `schema = true`; the project is then checked by the source's
|
|
248
|
+
item types, attributes, link types, statuses, transitions, roles, grounding and coverage rules,
|
|
249
|
+
plus whatever it adds itself. It may add an attribute, a type, a status or a link
|
|
250
|
+
type, but never redefine one the source declares: a widened enum or a changed
|
|
251
|
+
attribute is refused when the configuration is read, naming both values.
|
|
252
|
+
`tl migrate` removes a copy the source makes redundant, `tl context`,
|
|
253
|
+
`tl schema show` and `tl dump` mark what was inherited, and `tl schema` refuses
|
|
254
|
+
to change it. Every operation then reads that one source; the rest wait for the
|
|
255
|
+
union.
|
|
256
|
+
|
|
244
257
|
Sources are read-only, fetched once into a per-user cache and reached again only
|
|
245
258
|
when a tag or branch has moved; `TL_OFFLINE=1` composes from the cache alone. A
|
|
246
259
|
program embedding the Tool reaches a source through the `Resolver` it supplies, so
|
|
@@ -8,10 +8,10 @@ version control; a `check` command validates the whole graph and gates CI.
|
|
|
8
8
|
|
|
9
9
|
**Dogfooded:** throughline's own spec is itself a throughline project —
|
|
10
10
|
<!-- tl:count type == 'system_requirement' -->
|
|
11
|
-
|
|
11
|
+
248
|
|
12
12
|
<!-- tl:end --> system requirements,
|
|
13
13
|
<!-- tl:count type == 'user_requirement' -->
|
|
14
|
-
|
|
14
|
+
39
|
|
15
15
|
<!-- tl:end --> user requirements, and
|
|
16
16
|
<!-- tl:count type == 'nfr' -->
|
|
17
17
|
21
|
|
@@ -136,7 +136,7 @@ detect items that "violate" a non-goal — that judgement stays with a human.
|
|
|
136
136
|
```
|
|
137
137
|
tl init [--name NAME] # scaffold a project
|
|
138
138
|
tl register new <PREFIX> <dir> [--parent P] # add a register
|
|
139
|
-
tl new <PREFIX> [--uid U] [--type T] [--ground UID] # allocate + create (grounded at birth)
|
|
139
|
+
tl new <PREFIX> [--uid U] [--type T] [--ground UID] [--key UUID4] # allocate + create (grounded at birth); a held key creates nothing
|
|
140
140
|
tl link <SRC> <DST> --type <kind> [--stamp] # add a typed link
|
|
141
141
|
tl delete <UID> --reason "…" # propose a deletion: a tombstone a human ratifies (never erased)
|
|
142
142
|
tl restore <UID> # decline a proposed deletion
|
|
@@ -178,6 +178,7 @@ Upward and downward coverage are independent and both matter:
|
|
|
178
178
|
| `uid-filename` | an item's file is not named `<UID>.yml` (git could not see it collide) |
|
|
179
179
|
| `tombstone-deleted` | a retired UID's tombstone was erased (bad merge / stray `git rm`) |
|
|
180
180
|
| `schema` | missing required attr or out-of-enum value |
|
|
181
|
+
| `attribute-kind` | an attribute's value is not of its declared kind (`int`, `float`, `bool`, `date` as YYYY-MM-DD, `string`, `text`) |
|
|
181
182
|
| `suspect-link` | target changed since the link was last confirmed |
|
|
182
183
|
| `unreviewed` | item content changed since last review |
|
|
183
184
|
| `unratified` | AI-origin item still `proposed` |
|
|
@@ -213,6 +214,18 @@ reports every finding in `namespace:UID` vocabulary. `query`, `dump`, `docs`,
|
|
|
213
214
|
answer over the union too; everything else reads your graph alone, and a project
|
|
214
215
|
that declares no sources is untouched by any of this.
|
|
215
216
|
|
|
217
|
+
A graph whose own items follow a shared model — every deck in a series, every plan
|
|
218
|
+
written to one standard — can take its schema from a source instead of copying it.
|
|
219
|
+
Mark that one source `schema = true`; the project is then checked by the source's
|
|
220
|
+
item types, attributes, link types, statuses, transitions, roles, grounding and coverage rules,
|
|
221
|
+
plus whatever it adds itself. It may add an attribute, a type, a status or a link
|
|
222
|
+
type, but never redefine one the source declares: a widened enum or a changed
|
|
223
|
+
attribute is refused when the configuration is read, naming both values.
|
|
224
|
+
`tl migrate` removes a copy the source makes redundant, `tl context`,
|
|
225
|
+
`tl schema show` and `tl dump` mark what was inherited, and `tl schema` refuses
|
|
226
|
+
to change it. Every operation then reads that one source; the rest wait for the
|
|
227
|
+
union.
|
|
228
|
+
|
|
216
229
|
Sources are read-only, fetched once into a per-user cache and reached again only
|
|
217
230
|
when a tag or branch has moved; `TL_OFFLINE=1` composes from the cache alone. A
|
|
218
231
|
program embedding the Tool reaches a source through the `Resolver` it supplies, so
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "throughline"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.4.0"
|
|
8
8
|
description = "A Git-native requirements management tool with a scope-avalanche grounding layer: permanent UIDs, one file per item, typed links, suspect detection, and a CI-gating check."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -27,6 +27,14 @@ file the set had touched. Everything the set creates or changes is recorded
|
|
|
27
27
|
with a machine origin, so it enters the lifecycle as proposed and a person still
|
|
28
28
|
decides (SR-0092).
|
|
29
29
|
|
|
30
|
+
A set may be applied more than once and changes the graph once (SR-0280, SR-0281).
|
|
31
|
+
A ``new`` operation may carry a ``key``, a UUID version 4 recorded on the item it
|
|
32
|
+
creates; where a live item already carries that key the operation creates nothing,
|
|
33
|
+
and its label names that item. Any other operation whose result the graph already
|
|
34
|
+
holds — a link already there, an unlink of one that is not, an amend whose fields
|
|
35
|
+
already read so, a delete of a tombstone — is skipped and reported as already
|
|
36
|
+
applied, whatever revision it quotes, since nothing would be written.
|
|
37
|
+
|
|
30
38
|
These are the functions `tl apply` is made of; the command line is one caller
|
|
31
39
|
(SR-0224, SR-0225). A program embedding the Tool reaches them by name.
|
|
32
40
|
"""
|
|
@@ -40,7 +48,8 @@ from typing import Callable
|
|
|
40
48
|
|
|
41
49
|
from .fingerprint import fingerprint, revision
|
|
42
50
|
from .grounding import GroundingError, set_status
|
|
43
|
-
from .items import amend_item, delete_item, item_type_for,
|
|
51
|
+
from .items import (amend_item, check_key, delete_item, item_type_for, keyed_item,
|
|
52
|
+
new_item, parse_attrs)
|
|
44
53
|
from .links import GroundingView, LinkError, add_link, remove_link
|
|
45
54
|
from .model import Item, Link
|
|
46
55
|
from .schema import SchemaError
|
|
@@ -74,7 +83,7 @@ _DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
|
74
83
|
_KEYS: dict[str, tuple[set[str], set[str]]] = {
|
|
75
84
|
"new": ({"register"},
|
|
76
85
|
{"op", "register", "type", "title", "text", "rationale", "attrs",
|
|
77
|
-
"links", "label"}),
|
|
86
|
+
"links", "label", "key"}),
|
|
78
87
|
"amend": ({"uid"},
|
|
79
88
|
{"op", "uid", "title", "text", "rationale", "attrs",
|
|
80
89
|
"unset"}),
|
|
@@ -117,7 +126,10 @@ The five operations:
|
|
|
117
126
|
"links": [{{"target": "UR-0003", "type": "implements"}}], "label": "sr1"}}
|
|
118
127
|
creates an item in the register with that prefix. "type" may be left out
|
|
119
128
|
where the register declares one. "links" ground the item: a non-root item
|
|
120
|
-
must reach a root through them. "label" is optional.
|
|
129
|
+
must reach a root through them. "label" is optional. "key" is optional and
|
|
130
|
+
recommended: a fresh UUID version 4 (e.g. "3f0c1b7e-8a52-4c4e-9d61-2b7f5a9c0e14")
|
|
131
|
+
recorded on the item. Where an item already carries it, nothing is created
|
|
132
|
+
and the label names that item, so the set can be applied again safely.
|
|
121
133
|
|
|
122
134
|
{{"op": "amend", "uid": "SR-0012", "revision": "sha256:...", "title": "...",
|
|
123
135
|
"text": "...", "rationale": "...", "attrs": {{"priority": "must"}},
|
|
@@ -138,6 +150,10 @@ The five operations:
|
|
|
138
150
|
"reason": "..."}}
|
|
139
151
|
tombstones the item: its file stays and its UID is never reused.
|
|
140
152
|
|
|
153
|
+
Applying a set twice changes the graph once: an operation whose result the
|
|
154
|
+
graph already holds (a link already there, an amend whose fields already read
|
|
155
|
+
so, a delete of a deleted item) is skipped and reported as already applied.
|
|
156
|
+
|
|
141
157
|
Rules: no other keys; a "uid", "target" or "label" is written exactly; every
|
|
142
158
|
value is a string except "attrs" (strings, numbers or booleans), "stamp"
|
|
143
159
|
(boolean), and "unset" and "links" (lists). The list may also be sent as
|
|
@@ -229,6 +245,7 @@ def parse_change_set(text: str) -> list[dict]:
|
|
|
229
245
|
problems: list[str] = []
|
|
230
246
|
ops: list[dict] = []
|
|
231
247
|
labels: set[str] = set()
|
|
248
|
+
keys: set[str] = set()
|
|
232
249
|
for i, op in enumerate(data, start=1):
|
|
233
250
|
where = f"#{i}"
|
|
234
251
|
if not isinstance(op, dict):
|
|
@@ -255,7 +272,7 @@ def parse_change_set(text: str) -> list[dict]:
|
|
|
255
272
|
if missing:
|
|
256
273
|
problems.append(f"{name}: missing {', '.join(missing)}")
|
|
257
274
|
for key in ("uid", "target", "register", "type", "title", "text",
|
|
258
|
-
"rationale", "label", "reason", quoted):
|
|
275
|
+
"rationale", "label", "reason", "key", quoted):
|
|
259
276
|
if key in op and not isinstance(op[key], str):
|
|
260
277
|
problems.append(f"{name}: \"{key}\" must be a string")
|
|
261
278
|
if "stamp" in op and not isinstance(op["stamp"], bool):
|
|
@@ -289,6 +306,14 @@ def parse_change_set(text: str) -> list[dict]:
|
|
|
289
306
|
elif isinstance(op[quoted], str) and not _DIGEST_RE.match(op[quoted]):
|
|
290
307
|
problems.append(f"{name} {uid}: \"{quoted}\" must be the sha256:… "
|
|
291
308
|
"value the export gave")
|
|
309
|
+
if kind == "new" and isinstance(op.get("key"), str):
|
|
310
|
+
try:
|
|
311
|
+
check_key(op["key"])
|
|
312
|
+
except ValueError as e:
|
|
313
|
+
problems.append(f"{name}: {e}")
|
|
314
|
+
if op["key"] in keys:
|
|
315
|
+
problems.append(f"{name}: key {op['key']} is already used in this set")
|
|
316
|
+
keys.add(op["key"])
|
|
292
317
|
if kind == "new":
|
|
293
318
|
label = op.get("label")
|
|
294
319
|
if isinstance(label, str):
|
|
@@ -361,25 +386,83 @@ def apply_change_set(project, operations: list[dict], *,
|
|
|
361
386
|
ai = _ai_origin(schema)
|
|
362
387
|
problems: list[str] = []
|
|
363
388
|
labels: dict[str, str] = {}
|
|
364
|
-
|
|
365
|
-
|
|
389
|
+
# The operations whose result the graph already holds (SR-0280, SR-0281), by
|
|
390
|
+
# position: each is skipped rather than carried out.
|
|
391
|
+
held: set[int] = set()
|
|
392
|
+
# Held operations whose quoted version is no longer the item's: the mark of a
|
|
393
|
+
# set that has run before, as (position, name, uid, key, quoted, now).
|
|
394
|
+
stale: list[tuple[int, str, str, str, str, str]] = []
|
|
395
|
+
|
|
396
|
+
def version(op: dict, item: Item) -> tuple[str, str]:
|
|
397
|
+
key = "revision" if "revision" in op else "fingerprint"
|
|
398
|
+
return key, (revision(item) if key == "revision" else fingerprint(item, schema))
|
|
399
|
+
|
|
400
|
+
def resolved(ref: str) -> str | None:
|
|
401
|
+
"""A reference as it stands before anything is written: a UID, or the
|
|
402
|
+
existing item a label names, or ``None`` for an item still to be made."""
|
|
403
|
+
if not _is_label(ref):
|
|
404
|
+
return ref
|
|
405
|
+
return labels.get(ref[1:]) or None
|
|
406
|
+
|
|
407
|
+
def existing(name: str, op: dict, i: int) -> Item | None:
|
|
366
408
|
uid = op["uid"]
|
|
367
409
|
if _is_label(uid):
|
|
410
|
+
# A label naming an item the set found under its key is that item; an
|
|
411
|
+
# operation on it is judged as on any item created in the set, with
|
|
412
|
+
# no revision, but may already be held.
|
|
413
|
+
known = resolved(uid)
|
|
414
|
+
item = project.get(known) if known else None
|
|
415
|
+
if item is not None and holds(op, item):
|
|
416
|
+
held.add(i)
|
|
368
417
|
return None
|
|
369
418
|
item = project.get(uid)
|
|
370
419
|
if item is None:
|
|
371
420
|
problems.append(f"{name}: {uid} does not exist")
|
|
421
|
+
elif op["op"] == "delete" and item.is_deleted:
|
|
422
|
+
held.add(i)
|
|
372
423
|
elif item.is_deleted:
|
|
373
424
|
problems.append(f"{name}: {uid} is deleted — a tombstone is permanent")
|
|
425
|
+
elif holds(op, item):
|
|
426
|
+
held.add(i)
|
|
427
|
+
key, now = version(op, item)
|
|
428
|
+
if op.get(key) != now:
|
|
429
|
+
stale.append((i, name, uid, key, op.get(key), now))
|
|
374
430
|
else:
|
|
375
|
-
key =
|
|
376
|
-
now = revision(item) if key == "revision" else fingerprint(item, schema)
|
|
431
|
+
key, now = version(op, item)
|
|
377
432
|
if op.get(key) != now:
|
|
378
433
|
problems.append(f"{name}: {uid} changed since the export — the set "
|
|
379
434
|
f"quotes {key} {op.get(key)} but the item now "
|
|
380
435
|
f"carries {now}")
|
|
381
436
|
return item
|
|
382
437
|
|
|
438
|
+
def holds(op: dict, item: Item) -> bool:
|
|
439
|
+
"""Whether ``item`` already shows what ``op`` would make it (SR-0281)."""
|
|
440
|
+
kind = op["op"]
|
|
441
|
+
if kind == "delete":
|
|
442
|
+
return item.is_deleted
|
|
443
|
+
if kind in ("link", "unlink"):
|
|
444
|
+
target = resolved(op["target"])
|
|
445
|
+
if target is None:
|
|
446
|
+
return False
|
|
447
|
+
there = any(l.target == target and (op.get("type") is None
|
|
448
|
+
or l.type == op["type"])
|
|
449
|
+
for l in item.links)
|
|
450
|
+
return there if kind == "link" else not there
|
|
451
|
+
if kind == "amend":
|
|
452
|
+
for field_name in ("title", "text", "rationale"):
|
|
453
|
+
if field_name in op and getattr(item, field_name) != op[field_name]:
|
|
454
|
+
return False
|
|
455
|
+
try:
|
|
456
|
+
pairs = [f"{k}={v}" for k, v in (op.get("attrs") or {}).items()]
|
|
457
|
+
wanted = parse_attrs(schema, item.type, pairs, command="apply",
|
|
458
|
+
declared_only=True)
|
|
459
|
+
except (SchemaError, ValueError):
|
|
460
|
+
return False
|
|
461
|
+
if any(item.attrs.get(k) != v for k, v in wanted.items()):
|
|
462
|
+
return False
|
|
463
|
+
return not any(k in item.attrs for k in op.get("unset", ()))
|
|
464
|
+
return False
|
|
465
|
+
|
|
383
466
|
def target_known(name: str, ref: str) -> None:
|
|
384
467
|
if _is_label(ref) or find(ref) is not None:
|
|
385
468
|
return
|
|
@@ -398,15 +481,32 @@ def apply_change_set(project, operations: list[dict], *,
|
|
|
398
481
|
if schema.link_types and l["type"] not in schema.link_types:
|
|
399
482
|
problems.append(f"{name}: '{l['type']}' is not a link type this "
|
|
400
483
|
f"project declares ({', '.join(sorted(schema.link_types))})")
|
|
484
|
+
found = keyed_item(project, op["key"]) if "key" in op else None
|
|
485
|
+
if found is not None:
|
|
486
|
+
held.add(i)
|
|
401
487
|
if "label" in op:
|
|
402
|
-
labels[op["label"]] = ""
|
|
488
|
+
labels[op["label"]] = found.uid if found is not None else ""
|
|
403
489
|
continue
|
|
404
|
-
existing(name, op)
|
|
490
|
+
existing(name, op, i)
|
|
405
491
|
if kind in ("link", "unlink"):
|
|
406
492
|
target_known(name, op["target"])
|
|
407
493
|
if kind == "link" and schema.link_types and op["type"] not in schema.link_types:
|
|
408
494
|
problems.append(f"{name}: '{op['type']}' is not a link type this project "
|
|
409
495
|
f"declares ({', '.join(sorted(schema.link_types))})")
|
|
496
|
+
# A set that has run before can be applied again only where every item it
|
|
497
|
+
# creates carries a key (SR-0280): an unkeyed new operation cannot tell that
|
|
498
|
+
# it already ran, and would make its item a second time.
|
|
499
|
+
unkeyed = [f"#{i}" for i, op in enumerate(operations, start=1)
|
|
500
|
+
if op["op"] == "new" and "key" not in op]
|
|
501
|
+
if stale and unkeyed:
|
|
502
|
+
for _i, name, uid, key, quoted, now in stale:
|
|
503
|
+
problems.append(
|
|
504
|
+
f"{name}: {uid} changed since the export — the set quotes {key} "
|
|
505
|
+
f"{quoted} but the item now carries {now}. It already shows what "
|
|
506
|
+
f"this operation asks, so the set looks applied, and its new "
|
|
507
|
+
f"operation(s) {', '.join(unkeyed)} carry no key: applying it again "
|
|
508
|
+
"would create them twice. Nothing was changed. A set whose new "
|
|
509
|
+
"operations carry keys can be applied again safely (SR-0280)")
|
|
410
510
|
if problems:
|
|
411
511
|
raise ChangeSetError(problems)
|
|
412
512
|
|
|
@@ -445,6 +545,31 @@ def apply_change_set(project, operations: list[dict], *,
|
|
|
445
545
|
try:
|
|
446
546
|
for i, op in enumerate(operations, start=1):
|
|
447
547
|
kind = op["op"]
|
|
548
|
+
if i in held:
|
|
549
|
+
# Held when judged, and skipped if held still. Where an earlier
|
|
550
|
+
# operation in this set undid it, it is carried out as asked if the
|
|
551
|
+
# version it quoted was current, and otherwise refused: a set
|
|
552
|
+
# applied twice cannot tell that apart from a conflict.
|
|
553
|
+
if kind == "new":
|
|
554
|
+
found = keyed_item(project, op["key"])
|
|
555
|
+
if found is None:
|
|
556
|
+
raise ValueError(f"key {op['key']} no longer names an item")
|
|
557
|
+
if "label" in op:
|
|
558
|
+
labels[op["label"]] = found.uid
|
|
559
|
+
result.operations.append(OperationResult(
|
|
560
|
+
i, kind, found.uid, "already applied", f"key {op['key']}"))
|
|
561
|
+
continue
|
|
562
|
+
item = project.get(resolve(op["uid"]))
|
|
563
|
+
if item is not None and holds(op, item):
|
|
564
|
+
result.operations.append(OperationResult(
|
|
565
|
+
i, kind, item.uid, "already applied", ""))
|
|
566
|
+
continue
|
|
567
|
+
if i in {pos for pos, *_ in stale}:
|
|
568
|
+
raise ValueError("an earlier operation in this set undid what "
|
|
569
|
+
"this one found already done, and the "
|
|
570
|
+
"revision it quotes is not the item's")
|
|
571
|
+
# An earlier operation in the set undid it, and the version it
|
|
572
|
+
# quoted was current: it is carried out as asked.
|
|
448
573
|
if kind == "new":
|
|
449
574
|
prefix = op["register"]
|
|
450
575
|
item_type = item_type_for(project, prefix, op.get("type"))
|
|
@@ -452,7 +577,7 @@ def apply_change_set(project, operations: list[dict], *,
|
|
|
452
577
|
attrs = parse_attrs(schema, item_type, pairs, command="apply")
|
|
453
578
|
item = new_item(project, prefix, item_type=item_type,
|
|
454
579
|
title=op.get("title", ""), text=op.get("text", ""),
|
|
455
|
-
origin=ai, attrs=attrs)
|
|
580
|
+
origin=ai, attrs=attrs, key=op.get("key"))
|
|
456
581
|
if op.get("rationale"):
|
|
457
582
|
item.rationale = op["rationale"]
|
|
458
583
|
for l in op.get("links", []):
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import argparse
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
from .inherit import coverage_path
|
|
17
18
|
from .graph import Index
|
|
18
19
|
from .signatures import retired
|
|
19
20
|
from .parser import build_parser
|
|
@@ -106,7 +107,27 @@ def _ctx_working(schema) -> str:
|
|
|
106
107
|
)
|
|
107
108
|
|
|
108
109
|
|
|
109
|
-
def
|
|
110
|
+
def _from(inheritance, *path: str) -> str:
|
|
111
|
+
"""The mark on a declaration the schema source makes (SR-0279)."""
|
|
112
|
+
if inheritance is None or not inheritance.owns(*path):
|
|
113
|
+
return ""
|
|
114
|
+
return f" (from `{inheritance.namespace}`)"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _ctx_schema_source(inheritance) -> str:
|
|
118
|
+
"""Where this project's schema comes from (SR-0279), so an agent changes a rule
|
|
119
|
+
where it is declared rather than in a file that only inherits it."""
|
|
120
|
+
ns = inheritance.namespace
|
|
121
|
+
return (
|
|
122
|
+
"## Schema source\n\n"
|
|
123
|
+
f"This project takes its schema from the source `{ns}` (SR-0275) and declares "
|
|
124
|
+
"only what it adds. Below, every declaration marked _(from "
|
|
125
|
+
f"`{ns}`)_ is the source's: it cannot be redefined here (SR-0276), and "
|
|
126
|
+
"`tl schema` refuses to change it (SR-0279). Change it in the source's own "
|
|
127
|
+
"graph and move this project's pin to take it.")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _ctx_types(schema, inheritance=None) -> str:
|
|
110
131
|
out = ["## Item types and their attributes\n"]
|
|
111
132
|
if not schema.types:
|
|
112
133
|
out.append("_This project declares no per-type attribute schema; item "
|
|
@@ -120,6 +141,8 @@ def _ctx_types(schema) -> str:
|
|
|
120
141
|
tags.append("delivery-root")
|
|
121
142
|
if not schema.is_normative(tname):
|
|
122
143
|
tags.append("non-normative")
|
|
144
|
+
if _from(inheritance, "type", tname):
|
|
145
|
+
tags.append(f"from `{inheritance.namespace}`")
|
|
123
146
|
tag = f" _({', '.join(tags)})_" if tags else ""
|
|
124
147
|
out.append(f"### `{tname}`{tag}\n")
|
|
125
148
|
specs = schema.attrs_for(tname)
|
|
@@ -132,19 +155,30 @@ def _ctx_types(schema) -> str:
|
|
|
132
155
|
s = specs[aname]
|
|
133
156
|
vals = ", ".join(f"`{v}`" for v in s.values) if s.values else "—"
|
|
134
157
|
out.append(
|
|
135
|
-
f"| `{aname}`
|
|
158
|
+
f"| `{aname}`{_from(inheritance, 'attr', tname, aname)} | "
|
|
159
|
+
f"{s.kind or 'free'} | "
|
|
136
160
|
f"{'yes' if s.required else 'no'} | "
|
|
137
161
|
f"{'yes' if s.normative else 'no'} | {vals} |")
|
|
138
162
|
out.append("")
|
|
139
163
|
return "\n".join(out).rstrip()
|
|
140
164
|
|
|
141
165
|
|
|
142
|
-
def
|
|
166
|
+
def _inherited_of(inheritance, kind: str, names) -> str:
|
|
167
|
+
"""The members of ``names`` the schema source declares, as a brief line."""
|
|
168
|
+
if inheritance is None:
|
|
169
|
+
return ""
|
|
170
|
+
theirs = [n for n in names if inheritance.owns(kind, n)]
|
|
171
|
+
return (f" Of these, {_fmt_set(theirs)} come from the schema source "
|
|
172
|
+
f"`{inheritance.namespace}`." if theirs else "")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _ctx_links(schema, inheritance=None) -> str:
|
|
143
176
|
out = ["## Links\n"]
|
|
144
177
|
if schema.link_types is None:
|
|
145
178
|
out.append("- **Link vocabulary:** unconstrained (any link type is legal).")
|
|
146
179
|
else:
|
|
147
|
-
out.append(f"- **Link vocabulary:** {_fmt_set(schema.link_types)}."
|
|
180
|
+
out.append(f"- **Link vocabulary:** {_fmt_set(schema.link_types)}."
|
|
181
|
+
+ _inherited_of(inheritance, "link", schema.link_types))
|
|
148
182
|
out.append(f"- **Grounding links** (these carry justification): "
|
|
149
183
|
f"{_fmt_set(schema.ground_link_types)}.")
|
|
150
184
|
if schema.link_rules:
|
|
@@ -164,18 +198,20 @@ def _ctx_links(schema) -> str:
|
|
|
164
198
|
return "\n".join(out)
|
|
165
199
|
|
|
166
200
|
|
|
167
|
-
def _ctx_status(schema) -> str:
|
|
201
|
+
def _ctx_status(schema, inheritance=None) -> str:
|
|
168
202
|
out = ["## Status and lifecycle\n"]
|
|
169
203
|
if schema.statuses is None:
|
|
170
204
|
out.append("- **Statuses:** unconstrained (any status string is legal).")
|
|
171
205
|
else:
|
|
172
|
-
out.append(f"- **Statuses:** {_fmt_set(schema.statuses)}."
|
|
206
|
+
out.append(f"- **Statuses:** {_fmt_set(schema.statuses)}."
|
|
207
|
+
+ _inherited_of(inheritance, "status", schema.statuses))
|
|
173
208
|
if schema.transitions:
|
|
174
209
|
out.append("- **Allowed transitions** (a status may only move along "
|
|
175
210
|
"these edges; staying put is always allowed):\n")
|
|
176
211
|
for frm in sorted(schema.transitions):
|
|
177
212
|
tos = schema.transitions[frm]
|
|
178
|
-
out.append(f" - `{frm}` → {_fmt_set(tos)}"
|
|
213
|
+
out.append(f" - `{frm}` → {_fmt_set(tos)}"
|
|
214
|
+
+ _from(inheritance, "transition", frm))
|
|
179
215
|
out.append("\n Render this as a diagram with "
|
|
180
216
|
"`tl diagram transitions`.")
|
|
181
217
|
else:
|
|
@@ -247,7 +283,7 @@ def _describe_requirement(req) -> str:
|
|
|
247
283
|
return " and ".join(parts)
|
|
248
284
|
|
|
249
285
|
|
|
250
|
-
def _ctx_coverage(schema) -> str:
|
|
286
|
+
def _ctx_coverage(schema, inheritance=None) -> str:
|
|
251
287
|
out = ["## Coverage rules\n"]
|
|
252
288
|
if not schema.coverage:
|
|
253
289
|
out.append("_No `[[rules.coverage]]` declared._")
|
|
@@ -255,12 +291,19 @@ def _ctx_coverage(schema) -> str:
|
|
|
255
291
|
out.append("Each rule requires the matching items to have the stated link "
|
|
256
292
|
"(unmet → a `coverage` finding). They are numbered as "
|
|
257
293
|
"`tl schema rule remove` counts them:\n")
|
|
258
|
-
|
|
294
|
+
pos = 0
|
|
295
|
+
for rule in schema.coverage:
|
|
259
296
|
filt = rule.get("filter", "*")
|
|
260
297
|
needs = rule.get("needs", "?")
|
|
261
298
|
sev = rule.get("severity", WARNING)
|
|
262
|
-
|
|
263
|
-
|
|
299
|
+
line = f"items where `{filt}` **need** `{needs}` (severity: {sev})"
|
|
300
|
+
# An inherited rule is the schema source's (SR-0275, SR-0279): it is not
|
|
301
|
+
# this project's to remove, so it takes no number here.
|
|
302
|
+
if inheritance is not None and inheritance.owns(*coverage_path(rule)):
|
|
303
|
+
out.append(f"- {line} (from `{inheritance.namespace}`)")
|
|
304
|
+
continue
|
|
305
|
+
pos += 1
|
|
306
|
+
out.append(f"{pos}. {line}")
|
|
264
307
|
return "\n".join(out)
|
|
265
308
|
|
|
266
309
|
|
|
@@ -471,15 +514,17 @@ def context_markdown(project) -> str:
|
|
|
471
514
|
"configuration changes, regenerate it with `tl context`. "
|
|
472
515
|
"If a capability isn't described here, it isn't in the tool."
|
|
473
516
|
)
|
|
517
|
+
inheritance = project.inheritance
|
|
474
518
|
sections = [
|
|
475
519
|
header,
|
|
476
520
|
_ctx_idd(schema),
|
|
477
521
|
_ctx_working(schema),
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
522
|
+
*([_ctx_schema_source(inheritance)] if inheritance is not None else []),
|
|
523
|
+
_ctx_types(schema, inheritance),
|
|
524
|
+
_ctx_links(schema, inheritance),
|
|
525
|
+
_ctx_status(schema, inheritance),
|
|
481
526
|
_ctx_grounding(schema),
|
|
482
|
-
_ctx_coverage(schema),
|
|
527
|
+
_ctx_coverage(schema, inheritance),
|
|
483
528
|
_CTX_FORMAT,
|
|
484
529
|
_ctx_commands(),
|
|
485
530
|
_ctx_snapshot(project),
|
|
@@ -83,10 +83,12 @@ from .items import (
|
|
|
83
83
|
Amendment,
|
|
84
84
|
amend_item,
|
|
85
85
|
birth_item,
|
|
86
|
+
check_key,
|
|
86
87
|
coerce_attr,
|
|
87
88
|
delete_item,
|
|
88
89
|
restore_item,
|
|
89
90
|
item_type_for,
|
|
91
|
+
keyed_item,
|
|
90
92
|
new_item,
|
|
91
93
|
newly_suspect,
|
|
92
94
|
parse_attrs,
|
|
@@ -286,6 +288,7 @@ def cmd_migrate(args) -> int:
|
|
|
286
288
|
or result.cached or result.normative or result.contents
|
|
287
289
|
or result.unprovable or result.registers or result.lifted
|
|
288
290
|
or result.tombstones or result.unattributed or result.withdrawn
|
|
291
|
+
or result.inherited
|
|
289
292
|
else " — nothing to migrate"))
|
|
290
293
|
else:
|
|
291
294
|
# Already at this major, but missing configuration the major requires —
|
|
@@ -388,6 +391,13 @@ def cmd_migrate(args) -> int:
|
|
|
388
391
|
"declared source that carries it:")
|
|
389
392
|
for ns in result.withdrawn:
|
|
390
393
|
print(f" {ns}")
|
|
394
|
+
# The copy of the schema source's declarations removed (SR-0277), each named
|
|
395
|
+
# so the keeper can read the diff against this list.
|
|
396
|
+
if result.inherited:
|
|
397
|
+
print(f"removed {len(result.inherited)} declaration(s) the schema source "
|
|
398
|
+
"already makes identically:")
|
|
399
|
+
for what in result.inherited:
|
|
400
|
+
print(f" {what}")
|
|
391
401
|
if result.registers:
|
|
392
402
|
print(f"declared the item type of {len(result.registers)} register(s), "
|
|
393
403
|
"which `tl new` now gives items born there (SR-0241):")
|
|
@@ -453,6 +463,24 @@ def cmd_schema(args) -> int:
|
|
|
453
463
|
return OK
|
|
454
464
|
|
|
455
465
|
|
|
466
|
+
def cmd_schema_show(args) -> int:
|
|
467
|
+
"""List the project's schema as formed, each declaration marked with the schema
|
|
468
|
+
source it came from (SR-0279)."""
|
|
469
|
+
try:
|
|
470
|
+
project = schema_ops.load(Path(args.path))
|
|
471
|
+
except schema_ops.SchemaOpError as e:
|
|
472
|
+
return _err(str(e))
|
|
473
|
+
rows = schema_ops.show(project)
|
|
474
|
+
if args.format == "json":
|
|
475
|
+
print(json.dumps(rows, indent=2, ensure_ascii=False))
|
|
476
|
+
return OK
|
|
477
|
+
for row in rows:
|
|
478
|
+
value = "" if row["value"] is True else f" = {json.dumps(row['value'])}"
|
|
479
|
+
origin = f" (from {row['from']})" if row["from"] else ""
|
|
480
|
+
print(f"{row['declaration']}{value}{origin}")
|
|
481
|
+
return OK
|
|
482
|
+
|
|
483
|
+
|
|
456
484
|
def cmd_schema_attr_remove(args) -> int:
|
|
457
485
|
"""Withdraw an attribute from an item type, naming the items that still carry
|
|
458
486
|
it, and with --unset remove it from them as well (SR-0207).
|
|
@@ -466,7 +494,7 @@ def cmd_schema_attr_remove(args) -> int:
|
|
|
466
494
|
project = schema_ops.load(root)
|
|
467
495
|
declared = project.schema.attr(args.itype, args.name) is not None
|
|
468
496
|
carriers = schema_ops.attr_carriers(project, args.itype, args.name)
|
|
469
|
-
type_declared = args.itype in (project.
|
|
497
|
+
type_declared = args.itype in (project.formed_config.get("types") or {})
|
|
470
498
|
if declared:
|
|
471
499
|
change = schema_ops.attr_remove(project, args.itype, args.name)
|
|
472
500
|
elif args.unset and carriers and type_declared:
|
|
@@ -689,11 +717,23 @@ def cmd_new(args) -> int:
|
|
|
689
717
|
# honoured, including for a root type (SR-0091).
|
|
690
718
|
default_type = args.ground_type or "derives_from"
|
|
691
719
|
named = list(args.ground or ())
|
|
720
|
+
# The same creation asked for again (SR-0280): the item made under this key
|
|
721
|
+
# is named, and nothing is created or written.
|
|
722
|
+
key = getattr(args, "key", None)
|
|
723
|
+
if key is not None:
|
|
724
|
+
try:
|
|
725
|
+
check_key(key)
|
|
726
|
+
except ValueError as e:
|
|
727
|
+
return _err(str(e))
|
|
728
|
+
held = keyed_item(project, key)
|
|
729
|
+
if held is not None:
|
|
730
|
+
print(f"exists {held.uid} -> {held._path} (key {key}) — nothing created")
|
|
731
|
+
return OK
|
|
692
732
|
try:
|
|
693
733
|
item = new_item(project, args.prefix, item_type=item_type, uid=args.uid,
|
|
694
734
|
title=args.title or "", text=args.text or "",
|
|
695
735
|
status=args.status, origin=args.origin, attrs=attrs,
|
|
696
|
-
ground=named, ground_type=default_type)
|
|
736
|
+
ground=named, ground_type=default_type, key=key)
|
|
697
737
|
except (ProjectError, GroundingError, SchemaError, UidError) as e:
|
|
698
738
|
return _err(str(e))
|
|
699
739
|
grounds = [(target, default_type) for target in named]
|
|
@@ -478,7 +478,7 @@ def local_view(view: Project, consumer: Project) -> Project:
|
|
|
478
478
|
SR-0040). Ownership is read from the consumer's own register set rather than
|
|
479
479
|
inferred from the shape of a displayed name, so narrowing cannot disagree with
|
|
480
480
|
what was loaded."""
|
|
481
|
-
out =
|
|
481
|
+
out = view.shell()
|
|
482
482
|
for prefix, reg in view.registers.items():
|
|
483
483
|
if prefix in consumer.registers:
|
|
484
484
|
out.registers[prefix] = reg
|
|
@@ -56,7 +56,7 @@ def build_dump(project: Project, tool_version: str) -> dict:
|
|
|
56
56
|
d["revision"] = revision(it)
|
|
57
57
|
return d
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
out = {
|
|
60
60
|
"throughline_dump": {
|
|
61
61
|
"dump_schema_version": DUMP_SCHEMA_VERSION,
|
|
62
62
|
"format_version": FORMAT_VERSION,
|
|
@@ -66,3 +66,15 @@ def build_dump(project: Project, tool_version: str) -> dict:
|
|
|
66
66
|
"registers": registers,
|
|
67
67
|
"items": [entry(it) for it in items],
|
|
68
68
|
}
|
|
69
|
+
# A project that takes its schema from a source (SR-0275) says so, with the
|
|
70
|
+
# schema as formed and every declaration it inherited (SR-0279). `config`
|
|
71
|
+
# stays the project's own file. Absent for every other project, whose dump is
|
|
72
|
+
# unchanged.
|
|
73
|
+
inheritance = project.inheritance
|
|
74
|
+
if inheritance is not None:
|
|
75
|
+
out["schema_source"] = {
|
|
76
|
+
"namespace": inheritance.namespace,
|
|
77
|
+
"formed": project.formed_config,
|
|
78
|
+
"inherited": sorted(list(p) for p in inheritance.paths),
|
|
79
|
+
}
|
|
80
|
+
return out
|