Skip to content

Commit 849fe67

Browse files
committed
release: 0.3.0-recovery.6
1 parent 715e9f7 commit 849fe67

13 files changed

Lines changed: 254 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## 0.3.0-recovery.6
4+
5+
- Added a repeatable `--expected-root ROOT` option to `corpus verify`. Each
6+
supplied root is an assertion compared by index against the manifest
7+
header's declared root sequence before recorded-source verification; a
8+
count, lexical, real-path, or ordering mismatch returns a blocking
9+
`CI13_EXPECTED_ROOT_MISMATCH` integrity HOLD.
10+
- The option never relocates or rebinds file reads: verification still reads
11+
the absolute paths recorded in the manifest, and omitting `--expected-root`
12+
preserves the legacy no-flag behavior without binding verification to the
13+
caller's working directory, Git metadata, or environment.
14+
- Refreshed package, runtime, normative CLI output contract, README, and
15+
security-status version surfaces to the recovery.6 / 0.3.0rc6 candidate
16+
identity.
17+
18+
This remains a prerelease full-functional recovery candidate. Local
19+
prepublication validation does not imply production readiness, broad
20+
compatibility, external adoption, or human acceptance.
21+
322
## 0.3.0-recovery.5
423

524
- Made checkout-first onboarding explicit, added a progressive command map,

README.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ decisions.
1515
## Quick start
1616

1717
FABLE5 requires Python 3.11 or newer and has no non-standard-library Python
18-
runtime dependencies. Install the published prerelease in an isolated
19-
environment:
18+
runtime dependencies. Install the prerelease in an isolated environment:
2019

2120
```sh
2221
python3 -m venv .venv
2322
. .venv/bin/activate
24-
python -m pip install fable5-assurance-toolkit==0.3.0rc5
23+
python -m pip install fable5-assurance-toolkit==0.3.0rc6
2524
assurance --version
2625
assurance --help
2726
```
@@ -32,7 +31,7 @@ included in the wheel. For this prerelease, clone the matching release tag and
3231
enter the repository root:
3332

3433
```sh
35-
git clone --branch v0.3.0-recovery.5 --depth 1 \
34+
git clone --branch v0.3.0-recovery.6 --depth 1 \
3635
https://github.com/dormitivegit/fable5-assurance-toolkit.git
3736
cd fable5-assurance-toolkit
3837
```
@@ -101,6 +100,21 @@ The accepted SHA-256 is caller supplied. It anchors the exact manifest bytes
101100
and therefore the recorded scope declaration; it does not prove that the
102101
chosen roots or exclusions are complete, optimal, or authorized.
103102

103+
When a caller needs to assert the specific recorded subject before source
104+
verification, it can repeat `--expected-root ROOT` in manifest-root order:
105+
106+
```sh
107+
assurance corpus verify MANIFEST --expected-root ROOT [--expected-root ROOT ...]
108+
```
109+
110+
Each supplied root is normalized with the same lexical and real-path identity
111+
model used by PM-04, then compared by index with the manifest header's recorded
112+
`roots` and `real_roots`. A count, lexical, real-path, or ordering mismatch
113+
returns `CI13_EXPECTED_ROOT_MISMATCH` as an integrity-family `HOLD` (exit `4`)
114+
before PM-04 reads recorded source paths. Omitting the option preserves legacy
115+
recorded-path verification; the CLI never infers expected roots from CWD, Git,
116+
or the manifest location.
117+
104118
Corpus manifests bind sources to the absolute filesystem paths recorded at
105119
freeze time. Verification expects those paths to keep identifying the intended
106120
sources; a manifest is not a portable "freeze on one machine, verify under a
@@ -243,8 +257,8 @@ production. Those decisions remain with people responsible for the project.
243257
## Project status and provenance
244258

245259
```text
246-
PRODUCT_VERSION=0.3.0-recovery.5
247-
PYTHON_DISTRIBUTION_VERSION=0.3.0rc5
260+
PRODUCT_VERSION=0.3.0-recovery.6
261+
PYTHON_DISTRIBUTION_VERSION=0.3.0rc6
248262
STATUS=full-functional-recovery-candidate
249263
LINEAGE_ID=FABLE5-ASSURANCE-TOOLKIT-FULL-FUNCTIONAL-RECOVERY-20260713
250264
```
@@ -261,7 +275,12 @@ publishes the current contract hardening and consumer guidance. The published
261275
`v0.3.0-recovery.5` prerelease adds first-run navigation, validated
262276
maintainer-controlled workflow summaries, a runnable machine-consumer path,
263277
and PEP 517 distribution metadata. Its Python distribution is available from
264-
PyPI as `fable5-assurance-toolkit==0.3.0rc5`.
278+
PyPI as `fable5-assurance-toolkit==0.3.0rc5`. The `0.3.0-recovery.6`
279+
candidate adds the explicit expected-root subject assertion to
280+
`corpus verify`: a caller can require the manifest-declared root sequence
281+
before recorded-source verification. Omitting `--expected-root` preserves
282+
legacy behavior, and manifests remain bound to the absolute paths recorded at
283+
freeze time; they are not portable or rebindable.
265284

266285
The current status means the six modules and public interface have been
267286
reconstructed and mechanically tested. Bounded independent review exists for

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ publishes no guaranteed response or remediation SLA.
2222

2323
## Current security-support status
2424

25-
FABLE5 `0.3.0-recovery.5` has
25+
FABLE5 `0.3.0-recovery.6` has
2626
`STATUS=full-functional-recovery-candidate`. Its implemented public interface
2727
has mechanical test coverage, and the current contract/runtime correction has
2828
received bounded independent review. This is not comprehensive independent

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.0-recovery.5
1+
0.3.0-recovery.6

contracts/schemas/CLI_OUTPUT_CONTRACT.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"schema_version": "cli-output-contract/v2",
3-
"contract_id": "FABLE5_CLI_OUTPUT_AND_EXIT_CONTRACT_20260820",
3+
"contract_id": "FABLE5_CLI_OUTPUT_AND_EXIT_CONTRACT_20260823",
44
"normative": true,
55
"authority_direction": "PUBLISHED_CONTRACT_TO_TEST_EXPECTATION_TO_ACTUAL_CLI_OUTPUT",
66
"runtime_generation": {
7-
"product_version": "0.3.0-recovery.5",
8-
"python_distribution_version": "0.3.0rc5",
7+
"product_version": "0.3.0-recovery.6",
8+
"python_distribution_version": "0.3.0rc6",
99
"status": "full-functional-recovery-candidate"
1010
},
1111
"scope": {
@@ -50,7 +50,7 @@
5050
"FAIL",
5151
"HOLD"
5252
],
53-
"module_version": "0.3.0-recovery.5",
53+
"module_version": "0.3.0-recovery.6",
5454
"modules": {
5555
"PM-01": {
5656
"rule_set_version": "rr-v1-recovery",
@@ -263,7 +263,7 @@
263263
"eval",
264264
"pilot"
265265
],
266-
"module_version": "0.3.0-recovery.5",
266+
"module_version": "0.3.0-recovery.6",
267267
"rule_set_version": "recovery-1",
268268
"profile": "normal",
269269
"finding": {
@@ -304,8 +304,8 @@
304304
"--version"
305305
],
306306
"stdout_contains": [
307-
"assurance 0.3.0-recovery.5",
308-
"python-distribution-version: 0.3.0rc5",
307+
"assurance 0.3.0-recovery.6",
308+
"python-distribution-version: 0.3.0rc6",
309309
"status: full-functional-recovery-candidate"
310310
]
311311
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "fable5-assurance-toolkit"
7-
version = "0.3.0rc5"
7+
version = "0.3.0rc6"
88
description = "Local deterministic assurance controls reconstructed under a new lineage"
99
readme = "README.md"
1010
license = "Apache-2.0"

src/assurance_toolkit/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def build_parser() -> argparse.ArgumentParser:
114114
verify_parser = corpus_commands.add_parser("verify")
115115
verify_parser.add_argument("manifest")
116116
verify_parser.add_argument("--accepted-manifest-sha256", metavar="SHA256")
117+
verify_parser.add_argument("--expected-root", action="append", default=None, metavar="ROOT", help="assert a manifest root by index")
117118
verify_parser.add_argument("--detect-new", action="store_true")
118119
verify_parser.add_argument("--format", choices=("text", "json"), default="text")
119120

@@ -182,6 +183,7 @@ def main(argv: list[str] | None = None) -> int:
182183
args.manifest,
183184
args.detect_new,
184185
accepted_manifest_sha256=args.accepted_manifest_sha256,
186+
expected_roots=args.expected_root,
185187
).to_dict()
186188
elif args.command == "handoff":
187189
payload = validate_handoff(args.file, args.carrier, args.profile).to_dict()

src/assurance_toolkit/corpus.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def verify(
246246
manifest_path: str | Path,
247247
detect_new: bool = False,
248248
accepted_manifest_sha256: str | None = None,
249+
expected_roots: list[str | Path] | None = None,
249250
) -> ModuleResult:
250251
findings = []
251252
manifest = Path(manifest_path)
@@ -308,6 +309,67 @@ def verify(
308309
if record.get("record_type") not in {"manifest_header", "source_record", "duplicate_group", "manifest_summary"}:
309310
findings.append(finding("CI09_MALFORMED_MANIFEST", "ERROR", str(manifest), f"line {index}", "unknown manifest record type", record.get("record_type")))
310311

312+
if expected_roots is not None:
313+
roots = header.get("roots") if header else None
314+
real_roots = header.get("real_roots") if header else None
315+
header_is_supported = (
316+
bool(header)
317+
and header.get("schema_version") == SCHEMA_VERSION
318+
and header.get("rule_version") == RULE_VERSION
319+
)
320+
roots_are_valid = (
321+
isinstance(roots, list)
322+
and isinstance(real_roots, list)
323+
and all(isinstance(item, str) for item in roots)
324+
and all(isinstance(item, str) for item in real_roots)
325+
and len(roots) == len(real_roots)
326+
)
327+
if not header_is_supported or not roots_are_valid:
328+
if header_is_supported and not roots_are_valid:
329+
findings.append(finding(
330+
"CI09_MALFORMED_MANIFEST",
331+
"ERROR",
332+
str(manifest),
333+
"manifest_header.roots",
334+
"expected-root assertion requires equally sized string roots and real_roots arrays",
335+
{"roots_type": type(roots).__name__, "real_roots_type": type(real_roots).__name__},
336+
))
337+
sorted_items = sort_findings(findings)
338+
result, exit_code = outcome(sorted_items, "normal", family="integrity")
339+
return ModuleResult(result, MODULE_ID, rule_set_version=RULE_VERSION, findings=sorted_items, facts=[], exit_code=exit_code, data={"counts": {name.lower(): 0 for name in ("MATCH", "MISSING", "CHANGED", "TYPE_CHANGED", "SELF_INGESTED")}, "detect_new": detect_new})
340+
341+
expected_forms = [_path_forms(Path(item)) for item in expected_roots]
342+
expected_lexical = [str(lexical) for lexical, _ in expected_forms]
343+
expected_real = [str(real) for _, real in expected_forms]
344+
mismatch_classes = []
345+
if len(expected_lexical) != len(roots):
346+
mismatch_classes.append("CARDINALITY_MISMATCH")
347+
else:
348+
if expected_lexical != roots:
349+
mismatch_classes.append("LEXICAL_MISMATCH")
350+
if expected_real != real_roots:
351+
mismatch_classes.append("REAL_ROOT_MISMATCH")
352+
expected_pairs = list(zip(expected_lexical, expected_real))
353+
recorded_pairs = list(zip(roots, real_roots))
354+
if expected_pairs != recorded_pairs and sorted(expected_pairs) == sorted(recorded_pairs):
355+
mismatch_classes.append("ORDER_MISMATCH")
356+
if mismatch_classes:
357+
findings.append(finding(
358+
"CI13_EXPECTED_ROOT_MISMATCH",
359+
"HOLD",
360+
str(manifest),
361+
"expected_root",
362+
"caller expected roots do not match the manifest-declared root identities",
363+
{
364+
"mismatch_classes": mismatch_classes,
365+
"expected": {"roots": expected_lexical, "real_roots": expected_real},
366+
"recorded": {"roots": roots, "real_roots": real_roots},
367+
},
368+
))
369+
sorted_items = sort_findings(findings)
370+
result, exit_code = outcome(sorted_items, "normal", family="integrity")
371+
return ModuleResult(result, MODULE_ID, rule_set_version=RULE_VERSION, findings=sorted_items, facts=[], exit_code=exit_code, data={"counts": {name.lower(): 0 for name in ("MATCH", "MISSING", "CHANGED", "TYPE_CHANGED", "SELF_INGESTED")}, "detect_new": detect_new})
372+
311373
exclusions = [Path(value) for value in header.get("exclusions", []) if isinstance(value, str)]
312374
states: list[dict[str, Any]] = []
313375
source_records = [item for item in records if item.get("record_type") == "source_record"]

src/assurance_toolkit/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class PredicateResult:
3333
class ModuleResult:
3434
result: str
3535
module_id: str
36-
module_version: str = "0.3.0-recovery.5"
36+
module_version: str = "0.3.0-recovery.6"
3737
rule_set_version: str = "recovery-1"
3838
profile: str = "normal"
3939
findings: list[Finding] = field(default_factory=list)

src/assurance_toolkit/version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Product identity constants."""
22

3-
PRODUCT_VERSION = "0.3.0-recovery.5"
4-
PYTHON_DISTRIBUTION_VERSION = "0.3.0rc5"
3+
PRODUCT_VERSION = "0.3.0-recovery.6"
4+
PYTHON_DISTRIBUTION_VERSION = "0.3.0rc6"
55
STATUS = "full-functional-recovery-candidate"
66
LINEAGE_ID = "FABLE5-ASSURANCE-TOOLKIT-FULL-FUNCTIONAL-RECOVERY-20260713"

0 commit comments

Comments
 (0)