adopt-coverage 0.3.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.
- adopt_coverage/__init__.py +49 -0
- adopt_coverage/cache.py +85 -0
- adopt_coverage/py.typed +0 -0
- adopt_coverage/recompute.py +380 -0
- adopt_coverage/records.py +98 -0
- adopt_coverage-0.3.0.dist-info/METADATA +15 -0
- adopt_coverage-0.3.0.dist-info/RECORD +10 -0
- adopt_coverage-0.3.0.dist-info/WHEEL +4 -0
- adopt_coverage-0.3.0.dist-info/licenses/LICENSE +201 -0
- adopt_coverage-0.3.0.dist-info/licenses/NOTICE +39 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""`recompute_coverage` and the cache-disagreement alarm.
|
|
2
|
+
|
|
3
|
+
Implemented in S4. Contracts §6, implementation spec §4.8.
|
|
4
|
+
|
|
5
|
+
**The invariants this package carries.** It is the only writer of
|
|
6
|
+
`covered_cache` and `covered_cache_at` -- enforced by the `no-covered-cache-write`
|
|
7
|
+
import contract, not by convention. The recompute result is the authority and the
|
|
8
|
+
cache is rebuilt from it, never the reverse. A disagreement alarms and is never
|
|
9
|
+
silently reconciled, because a quietly self-healing cache reintroduces exactly
|
|
10
|
+
the invisible coverage decay the rebuild exists to delete.
|
|
11
|
+
|
|
12
|
+
**Computing and writing are two calls on purpose.** `recompute_coverage` reads
|
|
13
|
+
and decides; `rebuild_cache` writes. `store doctor` calls the first and not the
|
|
14
|
+
second, which is what lets it report a disagreement without destroying the
|
|
15
|
+
evidence of who caused it.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from adopt_coverage.cache import CacheWriter, rebuild_cache
|
|
19
|
+
from adopt_coverage.recompute import (
|
|
20
|
+
COVERAGE_REASONS,
|
|
21
|
+
REASON_AUDIENCE_OR_ENVIRONMENT,
|
|
22
|
+
REASON_IDENTITY_NOT_ACTIVE,
|
|
23
|
+
REASON_NO_ACTIVE_KNOWLEDGE_REVISION,
|
|
24
|
+
REASON_NO_LIVE_BINDING,
|
|
25
|
+
REASON_NO_OBSERVABILITY_BOUNDARY,
|
|
26
|
+
REASON_VERIFICATION_CONFLICTED,
|
|
27
|
+
CoverageResult,
|
|
28
|
+
Disagreement,
|
|
29
|
+
IdentityCoverage,
|
|
30
|
+
recompute_coverage,
|
|
31
|
+
)
|
|
32
|
+
from adopt_coverage.records import CoverageRecords
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"COVERAGE_REASONS",
|
|
36
|
+
"REASON_AUDIENCE_OR_ENVIRONMENT",
|
|
37
|
+
"REASON_IDENTITY_NOT_ACTIVE",
|
|
38
|
+
"REASON_NO_ACTIVE_KNOWLEDGE_REVISION",
|
|
39
|
+
"REASON_NO_LIVE_BINDING",
|
|
40
|
+
"REASON_NO_OBSERVABILITY_BOUNDARY",
|
|
41
|
+
"REASON_VERIFICATION_CONFLICTED",
|
|
42
|
+
"CacheWriter",
|
|
43
|
+
"CoverageRecords",
|
|
44
|
+
"CoverageResult",
|
|
45
|
+
"Disagreement",
|
|
46
|
+
"IdentityCoverage",
|
|
47
|
+
"rebuild_cache",
|
|
48
|
+
"recompute_coverage",
|
|
49
|
+
]
|
adopt_coverage/cache.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""The coverage cache write. **The only one in either repository.**
|
|
2
|
+
|
|
3
|
+
`no-covered-cache-write` scans every `.py` string literal and every `.sql` line
|
|
4
|
+
under `packages/`, `scripts/`, `tools/`, `bench/` and `schema/` for the cache
|
|
5
|
+
columns named alongside a write keyword, and `packages/adopt-coverage` is the
|
|
6
|
+
only source path it permits. That is why the statement is here and not beside the
|
|
7
|
+
other `identity` writes in `adopt_store.sqlite.records`, where it would be
|
|
8
|
+
rejected by the gate -- correctly, because a setter reachable from the store is a
|
|
9
|
+
setter every caller can reach.
|
|
10
|
+
|
|
11
|
+
**Why this module holds SQL when nothing else in the package does.** The
|
|
12
|
+
alternatives were each worse and are recorded so they are not re-proposed:
|
|
13
|
+
|
|
14
|
+
* a generic ``update_identity_columns(id, mapping)`` on the store passes the
|
|
15
|
+
regex while opening a *wider* hole than the specific setter it replaces --
|
|
16
|
+
every column becomes writable by every caller, which is dodging the gate
|
|
17
|
+
rather than satisfying it (CR-24: a gate people work around stops meaning
|
|
18
|
+
anything);
|
|
19
|
+
* splitting the statement so the column and the keyword land on different lines
|
|
20
|
+
is the same dodge, less visible;
|
|
21
|
+
* adding ``adopt_store.sqlite`` to the contract's ``allowed_paths`` makes the
|
|
22
|
+
write reachable by anyone holding a store, which is the invariant itself.
|
|
23
|
+
|
|
24
|
+
The executor is a **structural** protocol, so this package imports no store
|
|
25
|
+
module and no chain reaches `sqlite3` -- `no-raw-sqlite` names `adopt_coverage`
|
|
26
|
+
as a source module and would otherwise reject it.
|
|
27
|
+
|
|
28
|
+
**Known cost, stated rather than discovered later.** One SQL statement lives in a
|
|
29
|
+
package that is otherwise dialect-free, and the parameter marker differs under
|
|
30
|
+
psycopg. No sprint assigns the Postgres realization of coverage; whichever one
|
|
31
|
+
does inherits this seam and this note.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from collections.abc import Sequence
|
|
35
|
+
from contextlib import AbstractContextManager
|
|
36
|
+
from typing import Final, Protocol
|
|
37
|
+
|
|
38
|
+
from adopt_coverage.recompute import CoverageResult
|
|
39
|
+
from adopt_obs import format_timestamp
|
|
40
|
+
|
|
41
|
+
__all__ = ["CacheWriter", "rebuild_cache"]
|
|
42
|
+
|
|
43
|
+
#: The statement, in one place. Parameter order is (covered, at, identity_id).
|
|
44
|
+
_WRITE_CACHE: Final[str] = (
|
|
45
|
+
"UPDATE identity SET covered_cache = ?, covered_cache_at = ? WHERE id = ?"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CacheWriter(Protocol):
|
|
50
|
+
"""The two operations rebuilding the cache needs, and nothing else.
|
|
51
|
+
|
|
52
|
+
Satisfied structurally by `adopt_store.sqlite.store.SqliteStore`. Deliberately
|
|
53
|
+
**not** an import of that class: this package may not reach `sqlite3` even
|
|
54
|
+
transitively, and a protocol this narrow cannot be used to write anything the
|
|
55
|
+
caller did not already have the statement for.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def transaction(self) -> AbstractContextManager[None]: ...
|
|
59
|
+
def execute(self, sql: str, parameters: tuple[object, ...] = ()) -> None: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def rebuild_cache(writer: CacheWriter, result: CoverageResult) -> int:
|
|
63
|
+
"""Rebuild `covered_cache` from a recompute result. **Never the reverse.**
|
|
64
|
+
|
|
65
|
+
The direction is the whole contract (PRD F7.4, CUJ-3 step 4). This function
|
|
66
|
+
takes a `CoverageResult` and no store-read of its own precisely so that there
|
|
67
|
+
is no expression here in which the cache could influence the value written.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
writer: The store to write through.
|
|
71
|
+
result: What `recompute_coverage` decided.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
How many rows were written -- every identity in scope, not only the
|
|
75
|
+
disagreeing ones. A cache rebuilt only where it disagreed would leave
|
|
76
|
+
`covered_cache_at` lying about when the rest was last confirmed.
|
|
77
|
+
"""
|
|
78
|
+
stamp = format_timestamp(result.computed_at)
|
|
79
|
+
rows: Sequence[tuple[object, ...]] = [
|
|
80
|
+
(int(entry.covered), stamp, entry.identity_id) for entry in result.identities
|
|
81
|
+
]
|
|
82
|
+
with writer.transaction():
|
|
83
|
+
for parameters in rows:
|
|
84
|
+
writer.execute(_WRITE_CACHE, parameters)
|
|
85
|
+
return len(rows)
|
adopt_coverage/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""`recompute_coverage` -- the authority, and the cache-disagreement alarm.
|
|
2
|
+
|
|
3
|
+
**Breaking change 3 of 3, second half.** In the withdrawn `0.1.x` line
|
|
4
|
+
`identity_registry.covered` *was* truth, recomputed by whichever writer happened
|
|
5
|
+
to touch it. Here the function is truth and `identity.covered_cache` is a cache,
|
|
6
|
+
and the difference is the whole point: a cache that disagrees is a defect signal,
|
|
7
|
+
never a value to be quietly corrected.
|
|
8
|
+
|
|
9
|
+
**This module computes and never writes.** The write lives in
|
|
10
|
+
`adopt_coverage.cache`, one call away, so that `store doctor` can ask for the
|
|
11
|
+
comparison without the act of looking changing what is there. Implementation spec
|
|
12
|
+
§8's incident card is explicit -- rebuilding the cache first destroys the
|
|
13
|
+
evidence, and the writer that caused the drift is then unfindable.
|
|
14
|
+
|
|
15
|
+
**The six inputs are evaluated here, not in SQL.** The port hands back rows; each
|
|
16
|
+
predicate below is one input from contracts §6, named, so the property test that
|
|
17
|
+
compares this function against an independent reference implementation is
|
|
18
|
+
comparing two derivations rather than two callers of one clever query.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import datetime as _dt
|
|
22
|
+
from collections.abc import Mapping, Sequence
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Final
|
|
25
|
+
|
|
26
|
+
from adopt_const import COVERAGE_ALARM_SAMPLE_MAX
|
|
27
|
+
from adopt_coverage.records import CoverageRecords
|
|
28
|
+
from adopt_model import Binding, Identity, KnowledgeItem, ObservabilityBoundary
|
|
29
|
+
from adopt_obs import Clock, ErrorCode, SystemClock, get_logger, truncate_to_millisecond
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"COVERAGE_REASONS",
|
|
33
|
+
"REASON_AUDIENCE_OR_ENVIRONMENT",
|
|
34
|
+
"REASON_IDENTITY_NOT_ACTIVE",
|
|
35
|
+
"REASON_NO_ACTIVE_KNOWLEDGE_REVISION",
|
|
36
|
+
"REASON_NO_LIVE_BINDING",
|
|
37
|
+
"REASON_NO_OBSERVABILITY_BOUNDARY",
|
|
38
|
+
"REASON_VERIFICATION_CONFLICTED",
|
|
39
|
+
"CoverageResult",
|
|
40
|
+
"Disagreement",
|
|
41
|
+
"IdentityCoverage",
|
|
42
|
+
"recompute_coverage",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
_LOGGER: Final = get_logger("adopt.coverage")
|
|
46
|
+
|
|
47
|
+
# --------------------------------------------------------------------------
|
|
48
|
+
# The six inputs of contracts §6, one reason each.
|
|
49
|
+
#
|
|
50
|
+
# A reason names why an identity is **not** covered. They are stable strings
|
|
51
|
+
# because they reach the CLI envelope and a `store doctor` finding, and an
|
|
52
|
+
# operator branching on "which of the six is missing" is the whole reason the
|
|
53
|
+
# result is not a bare boolean.
|
|
54
|
+
# --------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
#: Input 1 -- "an active `identity_revision`".
|
|
57
|
+
REASON_IDENTITY_NOT_ACTIVE: Final[str] = "identity_revision_not_active"
|
|
58
|
+
|
|
59
|
+
#: Input 2 -- "at least one non-retired `binding`".
|
|
60
|
+
REASON_NO_LIVE_BINDING: Final[str] = "no_live_binding"
|
|
61
|
+
|
|
62
|
+
#: Input 3 -- "an active `knowledge_revision` on the bound item".
|
|
63
|
+
REASON_NO_ACTIVE_KNOWLEDGE_REVISION: Final[str] = "no_active_knowledge_revision"
|
|
64
|
+
|
|
65
|
+
#: Input 4 -- "applicable audience and environment".
|
|
66
|
+
REASON_AUDIENCE_OR_ENVIRONMENT: Final[str] = "audience_or_environment_inapplicable"
|
|
67
|
+
|
|
68
|
+
#: Input 5 -- "the `observability_boundary` for the scope".
|
|
69
|
+
REASON_NO_OBSERVABILITY_BOUNDARY: Final[str] = "no_observability_boundary"
|
|
70
|
+
|
|
71
|
+
#: Input 6 -- "verification requirements". A `conflicted` verification is Bet 4
|
|
72
|
+
#: working as designed: intent and reality disagree, the disagreement is
|
|
73
|
+
#: representable, and the identity is **not** reported as covered while it
|
|
74
|
+
#: stands. `unverified` passes, because it is the honest state every item starts
|
|
75
|
+
#: in and requiring `verified` would make coverage unreachable by construction.
|
|
76
|
+
REASON_VERIFICATION_CONFLICTED: Final[str] = "verification_conflicted"
|
|
77
|
+
|
|
78
|
+
#: Every reason, in evaluation order. Exported so a caller can enumerate the six
|
|
79
|
+
#: without re-deriving the list and getting five.
|
|
80
|
+
COVERAGE_REASONS: Final[tuple[str, ...]] = (
|
|
81
|
+
REASON_IDENTITY_NOT_ACTIVE,
|
|
82
|
+
REASON_NO_LIVE_BINDING,
|
|
83
|
+
REASON_NO_ACTIVE_KNOWLEDGE_REVISION,
|
|
84
|
+
REASON_AUDIENCE_OR_ENVIRONMENT,
|
|
85
|
+
REASON_NO_OBSERVABILITY_BOUNDARY,
|
|
86
|
+
REASON_VERIFICATION_CONFLICTED,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
#: The `identity_status` that counts as live. `moved` and `dead` do not: a moved
|
|
90
|
+
#: identity's coverage belongs to the identity it aliases, and a dead one covers
|
|
91
|
+
#: nothing.
|
|
92
|
+
_ACTIVE_IDENTITY_STATUS: Final[str] = "active"
|
|
93
|
+
|
|
94
|
+
#: The terminal `binding_status`. `active` and `moved` are both live -- a moved
|
|
95
|
+
#: binding still ties the item to the referent, which is what CUJ-2 turns on.
|
|
96
|
+
_RETIRED_BINDING_STATUS: Final[str] = "retired"
|
|
97
|
+
|
|
98
|
+
#: The terminal `freshness_state` on a knowledge item. Knowledge carries its
|
|
99
|
+
#: terminal state on the parent rather than on the revision (contracts §5
|
|
100
|
+
#: obligation 4), so this is where "the revision is not active" is read.
|
|
101
|
+
_RETIRED_ITEM_FRESHNESS: Final[str] = "retired"
|
|
102
|
+
|
|
103
|
+
#: The `verification` that blocks coverage.
|
|
104
|
+
_CONFLICTED_VERIFICATION: Final[str] = "conflicted"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class IdentityCoverage:
|
|
109
|
+
"""One identity's verdict, and why."""
|
|
110
|
+
|
|
111
|
+
identity_id: str
|
|
112
|
+
uri: str
|
|
113
|
+
covered: bool
|
|
114
|
+
#: Empty when covered. Sorted and deduplicated, so two runs over one store
|
|
115
|
+
#: produce one answer.
|
|
116
|
+
reasons: tuple[str, ...]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class Disagreement:
|
|
121
|
+
"""The cache said one thing and the recompute says another.
|
|
122
|
+
|
|
123
|
+
Alarm-grade on its own. Carries both values because "the cache is wrong" is
|
|
124
|
+
not actionable and "the cache says covered, the recompute says not" is.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
identity_id: str
|
|
128
|
+
uri: str
|
|
129
|
+
cached: bool
|
|
130
|
+
recomputed: bool
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True, slots=True)
|
|
134
|
+
class CoverageResult:
|
|
135
|
+
"""What `recompute_coverage` returns.
|
|
136
|
+
|
|
137
|
+
Nothing here is a cache and nothing here has been written anywhere. The
|
|
138
|
+
caller decides whether to rebuild the cache from it (`adopt_coverage.cache`)
|
|
139
|
+
or merely to look (`store doctor`).
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
system_id: str
|
|
143
|
+
environment_id: str | None
|
|
144
|
+
identities: tuple[IdentityCoverage, ...]
|
|
145
|
+
disagreements: tuple[Disagreement, ...]
|
|
146
|
+
computed_at: _dt.datetime
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def covered(self) -> int:
|
|
150
|
+
return sum(1 for entry in self.identities if entry.covered)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def uncovered(self) -> int:
|
|
154
|
+
return sum(1 for entry in self.identities if not entry.covered)
|
|
155
|
+
|
|
156
|
+
def verdict(self, identity_id: str) -> bool | None:
|
|
157
|
+
"""The verdict for one identity, or `None` when it is out of scope."""
|
|
158
|
+
for entry in self.identities:
|
|
159
|
+
if entry.identity_id == identity_id:
|
|
160
|
+
return entry.covered
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _boundary_applies(boundary: ObservabilityBoundary, environment_id: str) -> bool:
|
|
165
|
+
"""Whether a boundary row governs an identity in `environment_id`.
|
|
166
|
+
|
|
167
|
+
A boundary with no environment is the system-wide declaration and governs
|
|
168
|
+
every environment; one naming an environment governs only that one.
|
|
169
|
+
"""
|
|
170
|
+
return boundary.environment_id is None or boundary.environment_id == environment_id
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _environment_applies(item: KnowledgeItem, environment_id: str) -> bool:
|
|
174
|
+
"""Whether an item's environment is applicable to an identity's.
|
|
175
|
+
|
|
176
|
+
`knowledge_item.environment_id` is nullable *because an item may span
|
|
177
|
+
environments* -- so null is "applies everywhere", not "applies nowhere".
|
|
178
|
+
Reading it the other way would make every cross-environment item silently
|
|
179
|
+
stop covering anything.
|
|
180
|
+
"""
|
|
181
|
+
return item.environment_id is None or item.environment_id == environment_id
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _binding_blockers(
|
|
185
|
+
binding: Binding,
|
|
186
|
+
*,
|
|
187
|
+
binding_status: str | None,
|
|
188
|
+
item: KnowledgeItem | None,
|
|
189
|
+
verification: str | None,
|
|
190
|
+
has_verification_row: bool,
|
|
191
|
+
audience_count: int,
|
|
192
|
+
environment_id: str,
|
|
193
|
+
) -> frozenset[str]:
|
|
194
|
+
"""Inputs 2, 3, 4 and 6, for one candidate binding.
|
|
195
|
+
|
|
196
|
+
Returns the reasons this binding fails to carry coverage. Empty means it
|
|
197
|
+
carries it, and one such binding is enough -- contracts §6 asks for "at least
|
|
198
|
+
one non-retired binding", not for all of them.
|
|
199
|
+
"""
|
|
200
|
+
blockers: set[str] = set()
|
|
201
|
+
|
|
202
|
+
# Input 2 -- a binding whose head revision is retired is not live. A binding
|
|
203
|
+
# with no head revision at all is also not live: nothing has ever asserted
|
|
204
|
+
# the relationship.
|
|
205
|
+
if binding_status is None or binding_status == _RETIRED_BINDING_STATUS:
|
|
206
|
+
blockers.add(REASON_NO_LIVE_BINDING)
|
|
207
|
+
|
|
208
|
+
# Input 3 -- an active knowledge revision on the bound item.
|
|
209
|
+
if (
|
|
210
|
+
item is None
|
|
211
|
+
or item.current_revision_id is None
|
|
212
|
+
or item.freshness_state == _RETIRED_ITEM_FRESHNESS
|
|
213
|
+
):
|
|
214
|
+
blockers.add(REASON_NO_ACTIVE_KNOWLEDGE_REVISION)
|
|
215
|
+
# Inputs 4 and 6 are statements about that item. With no item there is
|
|
216
|
+
# nothing to say about them, and inventing a second reason would report
|
|
217
|
+
# one defect as three.
|
|
218
|
+
return frozenset(blockers)
|
|
219
|
+
|
|
220
|
+
# Input 4 -- applicable audience and environment.
|
|
221
|
+
if audience_count == 0 or not _environment_applies(item, environment_id):
|
|
222
|
+
blockers.add(REASON_AUDIENCE_OR_ENVIRONMENT)
|
|
223
|
+
|
|
224
|
+
# Input 6 -- verification requirements.
|
|
225
|
+
if has_verification_row and verification == _CONFLICTED_VERIFICATION:
|
|
226
|
+
blockers.add(REASON_VERIFICATION_CONFLICTED)
|
|
227
|
+
|
|
228
|
+
return frozenset(blockers)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _evaluate(
|
|
232
|
+
identity: Identity,
|
|
233
|
+
*,
|
|
234
|
+
identity_status: str | None,
|
|
235
|
+
bindings: Sequence[Binding],
|
|
236
|
+
binding_statuses: Mapping[str, str],
|
|
237
|
+
items: Mapping[str, KnowledgeItem],
|
|
238
|
+
verifications: Mapping[str, str | None],
|
|
239
|
+
audience_counts: Mapping[str, int],
|
|
240
|
+
boundaries: Sequence[ObservabilityBoundary],
|
|
241
|
+
) -> IdentityCoverage:
|
|
242
|
+
"""All six inputs for one identity."""
|
|
243
|
+
blockers: set[str] = set()
|
|
244
|
+
|
|
245
|
+
# Input 1 -- an active identity revision. An identity with no revision has
|
|
246
|
+
# never been asserted to exist by anything.
|
|
247
|
+
if identity_status != _ACTIVE_IDENTITY_STATUS:
|
|
248
|
+
blockers.add(REASON_IDENTITY_NOT_ACTIVE)
|
|
249
|
+
|
|
250
|
+
# Input 5 -- the observability boundary for the scope. Without one, nothing
|
|
251
|
+
# has declared what may be observed here, and coverage would be a claim
|
|
252
|
+
# about a system nobody agreed to look at.
|
|
253
|
+
if not any(_boundary_applies(row, identity.environment_id) for row in boundaries):
|
|
254
|
+
blockers.add(REASON_NO_OBSERVABILITY_BOUNDARY)
|
|
255
|
+
|
|
256
|
+
# Inputs 2, 3, 4 and 6, per candidate binding.
|
|
257
|
+
if not bindings:
|
|
258
|
+
blockers.add(REASON_NO_LIVE_BINDING)
|
|
259
|
+
else:
|
|
260
|
+
per_binding = [
|
|
261
|
+
_binding_blockers(
|
|
262
|
+
binding,
|
|
263
|
+
binding_status=binding_statuses.get(binding.id),
|
|
264
|
+
item=items.get(binding.item_id),
|
|
265
|
+
verification=verifications.get(binding.item_id),
|
|
266
|
+
has_verification_row=binding.item_id in verifications,
|
|
267
|
+
audience_count=audience_counts.get(binding.item_id, 0),
|
|
268
|
+
environment_id=identity.environment_id,
|
|
269
|
+
)
|
|
270
|
+
for binding in bindings
|
|
271
|
+
]
|
|
272
|
+
if all(reasons for reasons in per_binding):
|
|
273
|
+
# Every candidate failed. Report every distinct reason rather than
|
|
274
|
+
# the first: an operator fixing one binding's audience should not
|
|
275
|
+
# then discover the next binding was retired all along.
|
|
276
|
+
blockers.update(*per_binding)
|
|
277
|
+
|
|
278
|
+
return IdentityCoverage(
|
|
279
|
+
identity_id=identity.id,
|
|
280
|
+
uri=identity.uri,
|
|
281
|
+
covered=not blockers,
|
|
282
|
+
reasons=tuple(sorted(blockers)),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def recompute_coverage(
|
|
287
|
+
records: CoverageRecords,
|
|
288
|
+
system_id: str,
|
|
289
|
+
environment_id: str | None = None,
|
|
290
|
+
*,
|
|
291
|
+
clock: Clock | None = None,
|
|
292
|
+
) -> CoverageResult:
|
|
293
|
+
"""Evaluate coverage for every identity in scope. **The authority.**
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
records: The read port. Supplied rather than reached for, because a
|
|
297
|
+
module-level store would make this function untestable against the
|
|
298
|
+
random graphs its correctness property needs.
|
|
299
|
+
system_id: The system whose identities are evaluated.
|
|
300
|
+
environment_id: One environment, or `None` for every environment of the
|
|
301
|
+
system.
|
|
302
|
+
clock: Injected clock; tests pass `ManualClock`.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Per-identity coverage plus a `disagreements` list against
|
|
306
|
+
`covered_cache`. **Nothing is written.**
|
|
307
|
+
|
|
308
|
+
Emits:
|
|
309
|
+
`coverage_cache_disagreement` at `LogLevel.ALARM` when the disagreement
|
|
310
|
+
list is non-empty -- a defect signal that must page, not merely be
|
|
311
|
+
recorded (PRD F7.3). The **count is always complete**; the ids are a
|
|
312
|
+
sample bounded by `COVERAGE_ALARM_SAMPLE_MAX`, because a cold cache over
|
|
313
|
+
a 50k-identity store disagrees on every row and an uncapped field would
|
|
314
|
+
put a megabyte of ULIDs on one line. `store doctor` enumerates every
|
|
315
|
+
affected identity, so the alarm says *how bad* and the doctor says
|
|
316
|
+
*which*. Identity **ids** travel, never URIs: an id is minted by us and
|
|
317
|
+
carries no client-derived text.
|
|
318
|
+
"""
|
|
319
|
+
now = truncate_to_millisecond((clock if clock is not None else SystemClock()).now())
|
|
320
|
+
|
|
321
|
+
identities = records.identities_in_scope(system_id=system_id, environment_id=environment_id)
|
|
322
|
+
identity_statuses = records.head_identity_statuses(
|
|
323
|
+
system_id=system_id, environment_id=environment_id
|
|
324
|
+
)
|
|
325
|
+
binding_statuses = records.head_binding_statuses(
|
|
326
|
+
system_id=system_id, environment_id=environment_id
|
|
327
|
+
)
|
|
328
|
+
items = {row.id: row for row in records.items_in_scope(system_id=system_id)}
|
|
329
|
+
verifications = records.head_item_verifications(system_id=system_id)
|
|
330
|
+
audience_counts = records.audience_counts(system_id=system_id)
|
|
331
|
+
boundaries = records.boundaries_for_system(system_id=system_id)
|
|
332
|
+
|
|
333
|
+
bindings_by_identity: dict[str, list[Binding]] = {}
|
|
334
|
+
for binding in records.bindings_in_scope(system_id=system_id, environment_id=environment_id):
|
|
335
|
+
bindings_by_identity.setdefault(binding.identity_id, []).append(binding)
|
|
336
|
+
|
|
337
|
+
verdicts = tuple(
|
|
338
|
+
_evaluate(
|
|
339
|
+
identity,
|
|
340
|
+
identity_status=identity_statuses.get(identity.id),
|
|
341
|
+
bindings=bindings_by_identity.get(identity.id, []),
|
|
342
|
+
binding_statuses=binding_statuses,
|
|
343
|
+
items=items,
|
|
344
|
+
verifications=verifications,
|
|
345
|
+
audience_counts=audience_counts,
|
|
346
|
+
boundaries=boundaries,
|
|
347
|
+
)
|
|
348
|
+
for identity in identities
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
cached = {row.id: row.covered_cache for row in identities}
|
|
352
|
+
disagreements = tuple(
|
|
353
|
+
Disagreement(
|
|
354
|
+
identity_id=verdict.identity_id,
|
|
355
|
+
uri=verdict.uri,
|
|
356
|
+
cached=cached[verdict.identity_id],
|
|
357
|
+
recomputed=verdict.covered,
|
|
358
|
+
)
|
|
359
|
+
for verdict in verdicts
|
|
360
|
+
if cached[verdict.identity_id] != verdict.covered
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if disagreements:
|
|
364
|
+
_LOGGER.alarm(
|
|
365
|
+
"coverage_cache_disagreement",
|
|
366
|
+
code=str(ErrorCode.COVERAGE_CACHE_DISAGREEMENT),
|
|
367
|
+
system_id=system_id,
|
|
368
|
+
environment_id=environment_id,
|
|
369
|
+
disagreement_count=len(disagreements),
|
|
370
|
+
identity_ids=[entry.identity_id for entry in disagreements[:COVERAGE_ALARM_SAMPLE_MAX]],
|
|
371
|
+
identity_ids_truncated=len(disagreements) > COVERAGE_ALARM_SAMPLE_MAX,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
return CoverageResult(
|
|
375
|
+
system_id=system_id,
|
|
376
|
+
environment_id=environment_id,
|
|
377
|
+
identities=verdicts,
|
|
378
|
+
disagreements=disagreements,
|
|
379
|
+
computed_at=now,
|
|
380
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""The storage port `recompute_coverage` reads through.
|
|
2
|
+
|
|
3
|
+
Declared here rather than imported from `adopt_store`, following the precedent
|
|
4
|
+
`adopt_scope.records` set: `no-raw-sqlite` names `adopt_coverage` as a source
|
|
5
|
+
module and import-linter follows the chain, so a dependency on `adopt_store`
|
|
6
|
+
would reach `sqlite3` transitively and break the contract. A structural protocol
|
|
7
|
+
costs one file and keeps this package free of any driver.
|
|
8
|
+
|
|
9
|
+
**Every method is a read.** The cache write is not on this port -- it lives in
|
|
10
|
+
`adopt_coverage.cache`, which is the only place in either repository permitted to
|
|
11
|
+
hold the statement (`no-covered-cache-write`).
|
|
12
|
+
|
|
13
|
+
**The port fetches rows; it never decides.** Each method is one bulk read whose
|
|
14
|
+
result is a plain mapping or a sequence of generated models. Pushing any of the
|
|
15
|
+
six coverage inputs into SQL would move the authority out of
|
|
16
|
+
`recompute_coverage` and into whichever realization ran -- and the property test
|
|
17
|
+
that compares the function against an independent reference implementation would
|
|
18
|
+
then be comparing two callers of one query.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from collections.abc import Mapping, Sequence
|
|
22
|
+
from typing import Protocol
|
|
23
|
+
|
|
24
|
+
from adopt_model import Binding, Identity, KnowledgeItem, ObservabilityBoundary
|
|
25
|
+
|
|
26
|
+
__all__ = ["CoverageRecords"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CoverageRecords(Protocol):
|
|
30
|
+
"""Row in, decision out. No SQL, connection or cursor crosses this boundary.
|
|
31
|
+
|
|
32
|
+
Every method takes the scope the recompute was asked for. `environment_id`
|
|
33
|
+
is optional because `recompute_coverage` is (contracts §6); `None` means
|
|
34
|
+
every environment of the system rather than "the environment that is null",
|
|
35
|
+
and the two readings differ for `knowledge_item`, whose `environment_id` is
|
|
36
|
+
nullable precisely because an item may span environments.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def identities_in_scope(
|
|
40
|
+
self, *, system_id: str, environment_id: str | None
|
|
41
|
+
) -> Sequence[Identity]: ...
|
|
42
|
+
|
|
43
|
+
def systems_with_identities(self) -> Sequence[str]:
|
|
44
|
+
"""Every `system_id` that has at least one identity.
|
|
45
|
+
|
|
46
|
+
`store doctor` sweeps coverage across the whole store and has no scope
|
|
47
|
+
argument to work from (implementation spec §4.7: `doctor(store)`). Making
|
|
48
|
+
it ask which systems exist is what stops the sweep silently checking
|
|
49
|
+
nothing when a caller forgets to name one.
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def head_identity_statuses(
|
|
54
|
+
self, *, system_id: str, environment_id: str | None
|
|
55
|
+
) -> Mapping[str, str]:
|
|
56
|
+
"""`identity_id` -> the status of its head revision.
|
|
57
|
+
|
|
58
|
+
`identity` carries no head pointer, so the head is *derived*: the
|
|
59
|
+
revision no other revision supersedes (contracts §5 obligation 3). An
|
|
60
|
+
identity with no revision at all is absent from the mapping rather than
|
|
61
|
+
present with a placeholder -- "no revision" and "a revision saying
|
|
62
|
+
nothing" are different facts and the caller treats them differently.
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def bindings_in_scope(
|
|
67
|
+
self, *, system_id: str, environment_id: str | None
|
|
68
|
+
) -> Sequence[Binding]: ...
|
|
69
|
+
|
|
70
|
+
def head_binding_statuses(
|
|
71
|
+
self, *, system_id: str, environment_id: str | None
|
|
72
|
+
) -> Mapping[str, str]:
|
|
73
|
+
"""`binding_id` -> the status of its head revision."""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
def items_in_scope(self, *, system_id: str) -> Sequence[KnowledgeItem]:
|
|
77
|
+
"""Scoped by system only.
|
|
78
|
+
|
|
79
|
+
`knowledge_item.environment_id` is nullable, so filtering it by
|
|
80
|
+
environment here would silently drop every item that spans environments
|
|
81
|
+
-- which is the population the environment check in `recompute_coverage`
|
|
82
|
+
exists to reason about.
|
|
83
|
+
"""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
def head_item_verifications(self, *, system_id: str) -> Mapping[str, str | None]:
|
|
87
|
+
"""`item_id` -> `verification` on its current knowledge revision.
|
|
88
|
+
|
|
89
|
+
Absent when the item has no current revision; `None` when the revision
|
|
90
|
+
carries no verification, which the column permits.
|
|
91
|
+
"""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
def audience_counts(self, *, system_id: str) -> Mapping[str, int]:
|
|
95
|
+
"""`item_id` -> how many `audience_tag` rows it carries."""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
def boundaries_for_system(self, *, system_id: str) -> Sequence[ObservabilityBoundary]: ...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: adopt-coverage
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: recompute_coverage and the cache-disagreement alarm. Implemented in S4.
|
|
5
|
+
Project-URL: Homepage, https://github.com/onboardux/onboard-core
|
|
6
|
+
Project-URL: Source, https://github.com/onboardux/onboard-core
|
|
7
|
+
Project-URL: Issues, https://github.com/onboardux/onboard-core/issues
|
|
8
|
+
Author: The Adopt Authors
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: adopt-const
|
|
14
|
+
Requires-Dist: adopt-model
|
|
15
|
+
Requires-Dist: adopt-obs
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
adopt_coverage/__init__.py,sha256=B-m3s7xGuEa5ddl1cTD4x4-fbIEC5t4jfKXJxb0YZ-k,1723
|
|
2
|
+
adopt_coverage/cache.py,sha256=S_CV5gZUBuusO666FvP7W0LuyJf6Qxvm9Z8g2c5t6k8,3872
|
|
3
|
+
adopt_coverage/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
adopt_coverage/recompute.py,sha256=kYPMoP1D3unB3xhjWtNRJQQFZsoCLhjiErf9nd8SBSk,15036
|
|
5
|
+
adopt_coverage/records.py,sha256=Eq3yfzSj4oeyy1BA0mCOVrOlGTN1WK5G32gK3fFDpvk,4198
|
|
6
|
+
adopt_coverage-0.3.0.dist-info/METADATA,sha256=LgBJTD_3s3I3DocWFbfynGYjqhYr9pFC9ZhFOatglek,540
|
|
7
|
+
adopt_coverage-0.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
adopt_coverage-0.3.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
9
|
+
adopt_coverage-0.3.0.dist-info/licenses/NOTICE,sha256=2_mgo6v6IM9fAn52L5-wXFpISnC6PVU_geTutoRhbWk,1897
|
|
10
|
+
adopt_coverage-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Adopt — Adoption-Phase Platform, shared substrate (`adopt-core`)
|
|
2
|
+
Copyright 2026 The Adopt Authors
|
|
3
|
+
|
|
4
|
+
This product includes software developed by The Adopt Authors.
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
you may not use this file except in compliance with the License.
|
|
8
|
+
You may obtain a copy of the License at
|
|
9
|
+
|
|
10
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
See the License for the specific language governing permissions and
|
|
16
|
+
limitations under the License.
|
|
17
|
+
|
|
18
|
+
--------------------------------------------------------------------------------
|
|
19
|
+
Attribution note
|
|
20
|
+
--------------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
The copyright holder is recorded here as "The Adopt Authors" pending the legal
|
|
23
|
+
entity name. The owner must settle that attribution before the 0.3.0 tag,
|
|
24
|
+
because published package metadata cannot be changed retroactively for a
|
|
25
|
+
release that has already left the machine. The product name itself is settled:
|
|
26
|
+
handoff-index CR-50 keeps `Adopt` distinct from the `onboard` URI namespace.
|
|
27
|
+
|
|
28
|
+
--------------------------------------------------------------------------------
|
|
29
|
+
Third-party dependencies
|
|
30
|
+
--------------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
Every third-party dependency linked into this distribution is permissively
|
|
33
|
+
licensed. The complete list, with licence hash, security status, usage mode,
|
|
34
|
+
owner and re-verification date, is maintained in `licence-verifications.md` and
|
|
35
|
+
enforced by `scripts/licence_gate.py`.
|
|
36
|
+
|
|
37
|
+
Copyleft-licensed tools are invoked as subprocesses only and are never linked
|
|
38
|
+
into this distribution. They are declared in `subprocess-deps.toml` together
|
|
39
|
+
with their invocation sites.
|