Expand material support and checkpoint the completed building toolkit
Expose the runtime block/item registry through compact material search and single-material descriptions. Preserve private block-entity data through checked edits and durable undo without sending payloads to model context. Include the completed terrain tools, isolated world/map plugin, station lift, ACP streaming and guidance fixes, local camera auto-connect, construction scripts, and their public documentation, references and verification records. Active station decoration and private runtime data remain outside this commit. Validation: 145 Maven tests, 47 Bridge tests, successful camera Gradle build, and isolated Paper verification of all 1,196 block defaults plus 5,392 independent property cases for placement, same-material edits and restoration.
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only candidate/live station block, public-floor and containment QA.
|
||||
|
||||
Recipes use the checked layout contract {version:1,scope,blocks:[x,y,z,block,
|
||||
expected]}. Metadata declares public_floors, air-only clear_regions and optional
|
||||
containment bounds/seeds/authorized_caps. Flooding treats partial/unknown shapes
|
||||
as passable, so a successful result does not rely on decorative collision shapes.
|
||||
"""
|
||||
import argparse
|
||||
from collections import Counter, deque
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location('station_base', ROOT / 'scripts/verify-balustrade.py')
|
||||
base = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(base)
|
||||
survey, plaza = base.survey, base.plaza
|
||||
FULL = plaza.STATIC_CUBES | {'barrier'}
|
||||
N2 = ((1, 0), (-1, 0), (0, 1), (0, -1))
|
||||
N3 = ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1))
|
||||
|
||||
|
||||
def full_cube(state):
|
||||
return plaza.parse(state)[0] in FULL
|
||||
|
||||
|
||||
def bounded_box(document):
|
||||
box = survey.box_of(document['min'], document['max'])
|
||||
if survey.volume(box) > survey.MAX_VOXELS:
|
||||
raise ValueError('Verification box exceeds the snapshot voxel limit')
|
||||
return box
|
||||
|
||||
|
||||
def positions(box):
|
||||
return ((x, y, z) for x in range(box['min']['x'], box['max']['x'] + 1)
|
||||
for y in range(box['min']['y'], box['max']['y'] + 1)
|
||||
for z in range(box['min']['z'], box['max']['z'] + 1))
|
||||
|
||||
|
||||
def inside(at, box):
|
||||
return all(box['min'][axis] <= value <= box['max'][axis]
|
||||
for axis, value in zip(('x', 'y', 'z'), at))
|
||||
|
||||
|
||||
def surface_shape(state):
|
||||
if full_cube(state):
|
||||
return [(0., 1.)]
|
||||
name, props = plaza.parse(state)
|
||||
if name in {'smooth_sandstone_slab', 'cut_sandstone_slab', 'waxed_oxidized_cut_copper_slab'}:
|
||||
if props.get('waterlogged') != 'false':
|
||||
return None
|
||||
return {'bottom': [(0., .5)], 'top': [(.5, 1.)], 'double': [(0., 1.)]}.get(props.get('type'))
|
||||
return survey.vertical_shape(state)
|
||||
|
||||
|
||||
def public_floors(reader, floors):
|
||||
reports = []
|
||||
for floor in floors:
|
||||
name, feet, raw = floor['id'], floor['standing_y'], floor['clear_columns']
|
||||
if feet not in (99, 113):
|
||||
raise ValueError('This station stage declares public feet at Y99 or Y113')
|
||||
if (not isinstance(raw, list) or not raw or any(not isinstance(p, list) or len(p) != 2
|
||||
or any(type(v) is not int for v in p) for p in raw)):
|
||||
raise ValueError('Each public floor requires integer clear_columns [x,z]')
|
||||
clear = set(map(tuple, raw))
|
||||
if len(clear) != len(raw):
|
||||
raise ValueError('Duplicate public floor column')
|
||||
source = floor['source']['x'], floor['source']['z']
|
||||
if source not in clear:
|
||||
raise ValueError('Floor source is not a declared clear column')
|
||||
height = floor.get('min_headroom', 4)
|
||||
if type(height) not in (int, float) or not math.isfinite(height) or height < 1.8:
|
||||
raise ValueError('Headroom must be finite and at least 1.8 blocks')
|
||||
failures, valid = [], set()
|
||||
for x, z in sorted(clear):
|
||||
support = surface_shape(reader.state(x, feet - 1, z))
|
||||
reason = None
|
||||
if support is None or not any(abs(high - 1.) < 1e-8 for low, high in support):
|
||||
reason = 'missing_or_unknown_support_at_public_feet'
|
||||
else:
|
||||
for y in range(feet, math.ceil(feet + height)):
|
||||
shape = surface_shape(reader.state(x, y, z))
|
||||
if shape is None:
|
||||
reason = 'unknown_shape_in_required_clearance'
|
||||
break
|
||||
if any(y + lo < feet + height and y + hi > feet for lo, hi in shape):
|
||||
reason = 'occupied_required_clearance'
|
||||
break
|
||||
if reason:
|
||||
failures.append({'x': x, 'z': z, 'standing_y': feet, 'reason': reason})
|
||||
else:
|
||||
valid.add((x, z))
|
||||
reached, queue = set(), deque([source])
|
||||
while queue:
|
||||
p = queue.popleft()
|
||||
if p in reached or p not in valid:
|
||||
continue
|
||||
reached.add(p)
|
||||
queue.extend((p[0] + dx, p[1] + dz) for dx, dz in N2)
|
||||
unreachable = sorted(clear - reached)
|
||||
reports.append({'id': name, 'standing_y': feet, 'required_headroom': height,
|
||||
'declared_columns': len(clear), 'physically_clear_columns': len(valid),
|
||||
'reachable_clear_columns': len(reached), 'failed_samples': len(failures),
|
||||
'sample_failure_examples': failures[:30], 'unreachable_columns': len(unreachable),
|
||||
'unreachable_examples': unreachable[:30], 'passed': not failures and not unreachable})
|
||||
return {'status': 'checked' if reports else 'not_provided', 'passed': bool(reports) and all(r['passed'] for r in reports),
|
||||
'floors': reports, 'method': 'Observed center supports and declared vertical headroom, then cardinal traversal across valid level-floor columns. No lift transitions are inferred.'}
|
||||
|
||||
|
||||
def clear_regions(reader, regions):
|
||||
reports = []
|
||||
for region in regions:
|
||||
box, failures, checked = bounded_box(region), [], 0
|
||||
for at in positions(box):
|
||||
checked += 1
|
||||
state = reader.state(*at)
|
||||
if state not in survey.AIR:
|
||||
failures.append({'at': list(at), 'actual': state})
|
||||
reports.append({'id': region['id'], 'bounds': box, 'checked_voxels': checked,
|
||||
'nonair_voxels': len(failures), 'examples': failures[:30], 'passed': not failures})
|
||||
return {'status': 'checked' if reports else 'not_provided', 'passed': bool(reports) and all(r['passed'] for r in reports),
|
||||
'regions': reports, 'method': 'Every declared doorway/cabin/aisle clearance voxel must be observed air.'}
|
||||
|
||||
|
||||
def named_access(reader, metadata):
|
||||
"""Ensure furnishing targets are not silently filtered out of public topology."""
|
||||
interior = metadata.get('interior')
|
||||
if interior is None:
|
||||
return {'status': 'not_provided', 'passed': True}
|
||||
failures, targets_by_floor, lift_reports = [], {}, []
|
||||
floors = {f['standing_y']: set(map(tuple, f['clear_columns'])) for f in metadata['public_floors']}
|
||||
for raw_feet, navigation in interior['navigation'].items():
|
||||
feet = int(raw_feet)
|
||||
targets = navigation['named_targets']
|
||||
targets_by_floor[raw_feet] = len(targets)
|
||||
if feet not in floors or not targets:
|
||||
failures.append({'reason': 'missing_floor_or_named_targets', 'standing_y': feet})
|
||||
for target in targets:
|
||||
x, z = target['x'], target['z']
|
||||
if target['y'] != feet or (x, z) not in floors.get(feet, set()):
|
||||
failures.append({'reason': 'named_target_missing_from_public_topology', 'target': target})
|
||||
if not full_cube(reader.state(x, feet - 1, z)) or any(
|
||||
reader.state(x, y, z) not in survey.AIR for y in range(feet, feet + 4)):
|
||||
failures.append({'reason': 'named_target_missing_full_support_or_four_air', 'target': target})
|
||||
for level, lift in interior['lift'].items():
|
||||
selector = tuple(lift['selector_block'])
|
||||
landing = tuple(lift['landing_block'])
|
||||
selector_state = reader.state(*selector)
|
||||
x, feet, z = landing
|
||||
passed = selector_state == 'minecraft:gold_block' and full_cube(reader.state(x, feet - 1, z)) and all(
|
||||
reader.state(x, y, z) in survey.AIR for y in range(feet, feet + 4))
|
||||
lift_reports.append({'floor': level, 'selector': selector, 'observed_selector': selector_state,
|
||||
'landing': landing, 'passed': passed})
|
||||
if not passed:
|
||||
failures.append({'reason': 'lift_selector_or_landing_obstructed', 'floor': level})
|
||||
return {'status': 'checked', 'passed': not failures, 'named_targets_per_floor': targets_by_floor,
|
||||
'lift_landings': lift_reports, 'failures': failures,
|
||||
'method': 'Named furnishing targets must remain in the independently verified public-floor topology with full support and four air blocks. Gold selector and landing voxels are checked; runtime lift operation is not inferred.'}
|
||||
|
||||
|
||||
def fixture_stability(reader, desired):
|
||||
fixtures = {p: s for p, s in desired.items() if plaza.parse(s)[0] in {'lantern', 'spruce_leaves'}}
|
||||
report = plaza.check_fixtures(reader, fixtures, {'fixtures': []})
|
||||
report['checked'].pop('root_connected_logs', None)
|
||||
logs = {p: s for p, s in desired.items() if plaza.parse(s)[0] == 'spruce_log'}
|
||||
roots = [p for p in logs if (p[0], p[1] - 1, p[2]) not in logs]
|
||||
for x, y, z in roots:
|
||||
below = reader.state(x, y - 1, z)
|
||||
if plaza.parse(below)[0] not in {'dirt', 'moss_block'}:
|
||||
report['failures'].append({'reason': 'station_topiary_or_diorama_root_without_soil',
|
||||
'at': [x, y, z], 'below': below})
|
||||
report['checked'].update({'spruce_logs': len(logs), 'topiary_or_diorama_roots': len(roots)})
|
||||
report['passed'] = not report['failures']
|
||||
report['status'] = 'checked'
|
||||
report['method'] = 'Planned lantern support, persistent leaf distances against the full observed neighborhood, and station topiary/diorama roots on stable dirt or moss.'
|
||||
return report
|
||||
|
||||
|
||||
def conservative_containment(reader, metadata):
|
||||
if metadata is None:
|
||||
return {'status': 'not_proven', 'passed': False, 'reason': 'No containment bounds, seeds and authorized entrance caps were supplied.'}
|
||||
box = bounded_box(metadata['bounds'])
|
||||
caps, cap_reports = set(), []
|
||||
for region in metadata.get('authorized_caps', []):
|
||||
cap_box = bounded_box(region)
|
||||
points = set(positions(cap_box))
|
||||
if not points or not all(inside(p, box) for p in points):
|
||||
raise ValueError('Authorized virtual cap lies outside surveyed containment bounds')
|
||||
caps |= points
|
||||
cap_reports.append({'id': region['id'], 'bounds': cap_box, 'voxels': len(points)})
|
||||
blocked, passable, unknown = set(), set(), Counter()
|
||||
for at in positions(box):
|
||||
state = reader.state(*at)
|
||||
if full_cube(state) or at in caps:
|
||||
blocked.add(at)
|
||||
else:
|
||||
passable.add(at)
|
||||
if state not in survey.AIR:
|
||||
unknown[plaza.parse(state)[0]] += 1
|
||||
regions = [(r['id'], bounded_box(r)) for r in metadata.get('forbidden_regions', [])]
|
||||
roof_y = metadata.get('forbidden_y_at_or_above', 126)
|
||||
if type(roof_y) is not int:
|
||||
raise ValueError('Forbidden roof threshold must be an integer Y')
|
||||
seeds = metadata.get('seeds')
|
||||
if not isinstance(seeds, list) or not seeds:
|
||||
raise ValueError('Containment requires explicit public aisle/cabin seeds')
|
||||
reports = []
|
||||
for seed in seeds:
|
||||
coordinates = [seed[axis] for axis in ('x', 'y', 'z')]
|
||||
if any(type(v) not in (int, float) or not math.isfinite(v) for v in coordinates):
|
||||
raise ValueError('Seed coordinates must be finite')
|
||||
source = tuple(math.floor(v) for v in coordinates)
|
||||
if not inside(source, box) or source in caps:
|
||||
raise ValueError('Containment source is outside its observed box or inside a virtual cap')
|
||||
low, high = seed.get('min_y'), seed.get('max_y')
|
||||
if any(v is not None and type(v) is not int for v in (low, high)):
|
||||
raise ValueError('Per-floor minimum/maximum reachable Y must be integers')
|
||||
reached, queue, violations, examples = set(), deque([source]), Counter(), []
|
||||
while queue:
|
||||
p = queue.popleft()
|
||||
if p in reached or p not in passable:
|
||||
continue
|
||||
reached.add(p)
|
||||
reasons = []
|
||||
if any(value in (box['min'][axis], box['max'][axis]) for axis, value in zip(('x', 'y', 'z'), p)):
|
||||
reasons.append('survey_boundary_reachable')
|
||||
if p[1] >= roof_y:
|
||||
reasons.append('roof_space_reachable')
|
||||
if low is not None and p[1] < low:
|
||||
reasons.append('below_public_floor_reachable')
|
||||
if high is not None and p[1] > high:
|
||||
reasons.append('above_public_room_reachable')
|
||||
reasons.extend('forbidden_region:' + name for name, bounds in regions if inside(p, bounds))
|
||||
for reason in reasons:
|
||||
violations[reason] += 1
|
||||
if len(examples) < 30:
|
||||
examples.append({'at': list(p), 'reason': reason})
|
||||
queue.extend((p[0] + dx, p[1] + dy, p[2] + dz) for dx, dy, dz in N3)
|
||||
source_air = reader.state(*source) in survey.AIR
|
||||
reports.append({'id': seed['id'], 'source': list(source), 'source_observed_air': source_air,
|
||||
'reachable_voxels': len(reached), 'minimum_reached_y': min((p[1] for p in reached), default=None),
|
||||
'maximum_reached_y': max((p[1] for p in reached), default=None),
|
||||
'violation_counts': dict(violations), 'examples': examples,
|
||||
'passed': source_air and bool(reached) and not violations})
|
||||
return {'status': 'checked', 'passed': all(r['passed'] for r in reports), 'bounds': box,
|
||||
'observed_voxels': len(blocked) + len(passable), 'authorized_virtual_caps': cap_reports,
|
||||
'partial_or_unknown_materials_treated_as_passable': dict(unknown), 'seeds': reports,
|
||||
'method': 'Independent 6-neighbor floods from each public room/cabin seed. Known full cubes including glass block movement; partial/unknown shapes are treated as empty. Authorized entrance caps are virtual audit boundaries only.',
|
||||
'limits': 'This overestimates continuous player movement and does not simulate teleportation, spectator, block removal or lift operation. Failed floods through partial shapes require review; they are not automatically proven playable escape paths.'}
|
||||
|
||||
|
||||
def audit(before, recipe, metadata, after=None):
|
||||
scope = survey.scope_of(recipe['scope'])
|
||||
if scope != before.scope or scope != metadata['scope'] or (after and after.scope != scope):
|
||||
raise ValueError('Recipe, metadata and snapshots differ in project/world/epoch')
|
||||
desired = base.coordinates(recipe)
|
||||
for row in recipe['blocks']:
|
||||
at = tuple(row[axis] for axis in ('x', 'y', 'z'))
|
||||
if before.state(*at) != row['expected']:
|
||||
raise ValueError(f'Recipe expected state differs from observed baseline at {at}')
|
||||
reader = plaza.Reader(after or before, None if after else desired)
|
||||
floor_report = public_floors(reader, metadata.get('public_floors', []))
|
||||
clearance = clear_regions(reader, metadata.get('clear_regions', []))
|
||||
access = named_access(reader, metadata)
|
||||
fixtures = fixture_stability(reader, desired)
|
||||
containment = conservative_containment(reader, metadata.get('containment'))
|
||||
volume = base.compare_volume(before, after, desired) if after else None
|
||||
unproven = []
|
||||
if floor_report['status'] == 'not_provided':
|
||||
unproven.append('Public floor support/headroom/reachability: no clear-column topology supplied.')
|
||||
if clearance['status'] == 'not_provided':
|
||||
unproven.append('Doorway and lift cabin clearances: no explicit clearance regions supplied.')
|
||||
if containment['status'] == 'not_proven':
|
||||
unproven.append('Windows, floor separation and roof/private-space containment: no flood contract supplied.')
|
||||
provided = [r for r in (floor_report, clearance, access, fixtures, containment) if r['status'] == 'checked']
|
||||
supported_passed = all(r['passed'] for r in provided) and (volume is None or volume['passed'])
|
||||
return {'version': 1, 'scope': scope, 'world_edits': 0,
|
||||
'mode': 'actual_after_snapshot' if after else 'candidate_overlay', 'desired_blocks': len(desired),
|
||||
'expected_states_verified_against_baseline': len(desired), 'public_floors': floor_report,
|
||||
'clear_regions': clearance, 'named_access': access, 'fixtures': fixtures,
|
||||
'containment': containment, 'volume': volume,
|
||||
'supported_checks_passed': supported_passed, 'unproven_checks': unproven,
|
||||
'passed': supported_passed and not unproven,
|
||||
'limits': 'Only observed block states are verified. Block entity text, display entities, gameplay/sign destinations and operational lift transitions require separate runtime checks. Actual snapshots are sequential, not atomic.'}
|
||||
|
||||
|
||||
class FakeReader:
|
||||
def __init__(self, states):
|
||||
self.states = states
|
||||
|
||||
def state(self, x, y, z):
|
||||
return self.states.get((x, y, z), 'minecraft:air')
|
||||
|
||||
|
||||
class StationTests(unittest.TestCase):
|
||||
def room(self):
|
||||
box = {'min': {'x': -1, 'y': 98, 'z': -1}, 'max': {'x': 5, 'y': 127, 'z': 5}}
|
||||
states = {(x, y, z): 'minecraft:glass' for x in range(5) for y in range(99, 104) for z in range(5)
|
||||
if x in (0, 4) or z in (0, 4) or y in (99, 103)}
|
||||
meta = {'bounds': box, 'seeds': [{'id': 'room', 'x': 2, 'y': 100, 'z': 2, 'min_y': 100, 'max_y': 102}]}
|
||||
return FakeReader(states), meta
|
||||
|
||||
def test_closed_glass_room_is_contained_and_window_hole_is_detected(self):
|
||||
reader, metadata = self.room()
|
||||
self.assertTrue(conservative_containment(reader, metadata)['passed'])
|
||||
del reader.states[0, 101, 2]
|
||||
result = conservative_containment(reader, metadata)
|
||||
self.assertFalse(result['passed'])
|
||||
self.assertIn('survey_boundary_reachable', result['seeds'][0]['violation_counts'])
|
||||
|
||||
def test_authorized_entrance_can_be_virtually_capped_without_changing_world(self):
|
||||
reader, metadata = self.room()
|
||||
del reader.states[0, 101, 2]
|
||||
metadata['authorized_caps'] = [{'id': 'entry', 'min': {'x': 0, 'y': 101, 'z': 2}, 'max': {'x': 0, 'y': 101, 'z': 2}}]
|
||||
self.assertTrue(conservative_containment(reader, metadata)['passed'])
|
||||
self.assertEqual(reader.state(0, 101, 2), 'minecraft:air')
|
||||
|
||||
def test_partial_window_is_treated_as_open_and_upper_floor_hole_is_detected(self):
|
||||
reader, metadata = self.room()
|
||||
reader.states[0, 101, 2] = 'minecraft:iron_bars[east=false,north=true,south=true,waterlogged=false,west=false]'
|
||||
self.assertFalse(conservative_containment(reader, metadata)['passed'])
|
||||
reader, metadata = self.room()
|
||||
del reader.states[2, 99, 2]
|
||||
result = conservative_containment(reader, metadata)
|
||||
self.assertIn('below_public_floor_reachable', result['seeds'][0]['violation_counts'])
|
||||
|
||||
def test_public_headroom_is_four_blocks_and_obstacle_disconnects_floor(self):
|
||||
reader = FakeReader({(x, 98, 0): 'minecraft:stone' for x in range(3)})
|
||||
floor = {'id': 'vestibule', 'standing_y': 99, 'source': {'x': 0, 'z': 0}, 'clear_columns': [[0, 0], [1, 0], [2, 0]]}
|
||||
self.assertTrue(public_floors(reader, [floor])['passed'])
|
||||
reader.states[1, 102, 0] = 'minecraft:lantern[hanging=true,waterlogged=false]'
|
||||
result = public_floors(reader, [floor])
|
||||
self.assertFalse(result['passed'])
|
||||
self.assertEqual(result['floors'][0]['unreachable_columns'], 2)
|
||||
|
||||
def test_clearance_region_reports_real_door_obstruction(self):
|
||||
reader = FakeReader({(0, 99, 0): 'minecraft:stone'})
|
||||
region = {'id': 'door', 'min': {'x': 0, 'y': 99, 'z': 0}, 'max': {'x': 0, 'y': 102, 'z': 0}}
|
||||
self.assertEqual(clear_regions(reader, [region])['regions'][0]['nonair_voxels'], 1)
|
||||
|
||||
def test_named_target_cannot_be_hidden_by_filtering_the_public_mask(self):
|
||||
reader = FakeReader({(x, 98, 0): 'minecraft:stone' for x in range(2)})
|
||||
metadata = {'public_floors': [{'standing_y': 99, 'clear_columns': [[0, 0]]}],
|
||||
'interior': {'navigation': {'99': {'named_targets': [
|
||||
{'name': 'bench-front', 'x': 1, 'y': 99, 'z': 0}]}}, 'lift': {}}}
|
||||
self.assertFalse(named_access(reader, metadata)['passed'])
|
||||
metadata['public_floors'][0]['clear_columns'].append([1, 0])
|
||||
self.assertTrue(named_access(reader, metadata)['passed'])
|
||||
|
||||
def test_fixture_leaf_distance_and_lantern_support_use_actual_states(self):
|
||||
desired = {(0, 100, 0): 'minecraft:lantern[hanging=true,waterlogged=false]',
|
||||
(2, 100, 0): 'minecraft:spruce_leaves[distance=1,persistent=true,waterlogged=false]'}
|
||||
reader = FakeReader(desired | {(0, 101, 0): 'minecraft:stone',
|
||||
(2, 99, 0): 'minecraft:spruce_log[axis=y]'})
|
||||
self.assertTrue(fixture_stability(reader, desired)['passed'])
|
||||
del reader.states[0, 101, 0]
|
||||
del reader.states[2, 99, 0]
|
||||
self.assertEqual(len(fixture_stability(reader, desired)['failures']), 2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
for name in ('before', 'after', 'recipe', 'metadata', 'report'):
|
||||
parser.add_argument('--' + name, type=Path)
|
||||
parser.add_argument('--self-test', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if args.self_test:
|
||||
result = unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(StationTests))
|
||||
raise SystemExit(0 if result.wasSuccessful() else 1)
|
||||
if any(getattr(args, name) is None for name in ('before', 'recipe', 'metadata', 'report')):
|
||||
parser.error('--before, --recipe, --metadata and --report are required')
|
||||
before = survey.load_snapshot(args.before)
|
||||
after = survey.load_snapshot(args.after, before.scope) if args.after else None
|
||||
report = audit(before, json.loads(args.recipe.read_text()), json.loads(args.metadata.read_text()), after)
|
||||
paths = {name: getattr(args, name) for name in ('before', 'after', 'recipe', 'metadata') if getattr(args, name)}
|
||||
report['input_sha256'] = {key: hashlib.sha256(path.read_bytes()).hexdigest() for key, path in paths.items()}
|
||||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
survey.terrain.save(args.report, report)
|
||||
print(json.dumps(report, indent=2))
|
||||
raise SystemExit(0 if report['passed'] else 2 if report['supported_checks_passed'] and report['unproven_checks'] else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user