@arbiterforge/ca-pi 0.15.12 → 0.16.1
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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +33 -0
- package/plugins/ca-pi/SKILLS.md +1 -1
- package/plugins/ca-pi/agents/infra-author.md +7 -1
- package/plugins/ca-pi/agents/scout.md +40 -8
- package/plugins/ca-pi/generated/command-catalog.json +1 -1
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +104 -6
- package/plugins/ca-pi/hooks/_activationlib.py +97 -13
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +313 -76
- package/plugins/ca-pi/hooks/_artifactlib.py +166 -2
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
- package/plugins/ca-pi/hooks/_bashguardlib.py +14 -0
- package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
- package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
- package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
- package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
- package/plugins/ca-pi/hooks/_hooklib.py +1 -0
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_intentlib.py +5 -5
- package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
- package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
- package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
- package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
- package/plugins/ca-pi/hooks/artifact-authority.py +20 -1
- package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
- package/plugins/ca-pi/hooks/doctor.py +4 -2
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
- package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
- package/plugins/ca-pi/hooks/session-start.py +26 -7
- package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
- package/plugins/ca-pi/includes/helper-invocation.md +128 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
- package/plugins/ca-pi/includes/routing-table.md +1 -1
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
- package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
- package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
- package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
- package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
- package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
- package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
- package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +55 -8
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# codeArbiter -- bounded, pure decoding of internal scout evidence reports.
|
|
3
3
|
#
|
|
4
|
-
# Public API: decode_report(payload, *, expected) -> detached normalized dict
|
|
4
|
+
# Public API: decode_report(payload, *, expected) -> detached normalized dict;
|
|
5
|
+
# admit_scout_attempt and join_scout_reports validate a bounded host-owned run;
|
|
6
|
+
# validate_command_record and validate_constraint_reference normalize inert
|
|
7
|
+
# derived records without changing the v1 transport schema.
|
|
8
|
+
# resolve_scoped_command_guidance selects one source-bound command and reports
|
|
9
|
+
# method-aware execution evidence without executing it or granting authority.
|
|
5
10
|
# ReportError(code, field) reports only bounded code-owned diagnostics.
|
|
6
11
|
# No filesystem, subprocess, network, dispatch, approval, or artifact mutation.
|
|
7
12
|
# A matching supplied identity is not proof that the host actually inspected it.
|
|
8
13
|
# This codec admits report transport; category gaps remain gaps. It does not
|
|
9
|
-
# establish
|
|
14
|
+
# establish host containment, truth, freshness, or readiness.
|
|
10
15
|
|
|
11
16
|
from __future__ import annotations
|
|
12
17
|
|
|
18
|
+
import hashlib
|
|
13
19
|
import json
|
|
14
20
|
import re
|
|
15
21
|
import unicodedata
|
|
16
22
|
|
|
17
23
|
SCHEMA_VERSION = 1
|
|
18
|
-
MAX_REPORT_BYTES =
|
|
24
|
+
MAX_REPORT_BYTES = 64 * 1024
|
|
19
25
|
MAX_DEPTH = 12
|
|
20
26
|
MAX_NODES = 4096
|
|
21
27
|
MAX_PATHS = 16
|
|
22
|
-
|
|
28
|
+
MAX_REPORT_EVIDENCE = 128
|
|
29
|
+
MAX_EVIDENCE = 64 # Derived command/constraint record references, not scout reports.
|
|
23
30
|
MAX_FINDINGS = 64
|
|
24
31
|
CATEGORIES = frozenset({'stack', 'infrastructure', 'architecture', 'security', 'tests', 'data'})
|
|
25
32
|
OUTCOMES = frozenset({'observed', 'not_applicable', 'not_found_in_scope',
|
|
@@ -176,7 +183,7 @@ def _coverage(value, scope, field):
|
|
|
176
183
|
|
|
177
184
|
def _evidence(value, coverage, scope, field):
|
|
178
185
|
result = []
|
|
179
|
-
for index, entry in enumerate(_list(value,
|
|
186
|
+
for index, entry in enumerate(_list(value, MAX_REPORT_EVIDENCE, field)):
|
|
180
187
|
part = field+'['+str(index)+']'
|
|
181
188
|
_object(entry, ('id', 'path', 'start_line', 'end_line', 'digest_method', 'digest'), part)
|
|
182
189
|
path = _path(entry['path'], part+'.path')
|
|
@@ -215,7 +222,7 @@ def _outcome(value, scope, field):
|
|
|
215
222
|
for index, entry in enumerate(_list(value['findings'], MAX_FINDINGS, field+'.findings')):
|
|
216
223
|
part = field+'.findings['+str(index)+']'
|
|
217
224
|
_object(entry, ('id', 'name', 'value', 'evidence_refs'), part)
|
|
218
|
-
refs = [_id(ref, part+'.evidence_refs') for ref in _list(entry['evidence_refs'],
|
|
225
|
+
refs = [_id(ref, part+'.evidence_refs') for ref in _list(entry['evidence_refs'], MAX_REPORT_EVIDENCE, part)]
|
|
219
226
|
_unique(refs, part+'.evidence_refs')
|
|
220
227
|
_need(bool(refs) and set(refs) <= evidence_ids, 'DANGLING_EVIDENCE', part)
|
|
221
228
|
used.update(refs)
|
|
@@ -329,6 +336,8 @@ def decode_report(payload, *, expected):
|
|
|
329
336
|
_unique([o['category'] for o in outcomes], 'report.outcomes')
|
|
330
337
|
_need({o['category'] for o in outcomes} == set(binding['scope']['categories']),
|
|
331
338
|
'CATEGORY_COVERAGE', 'report.outcomes')
|
|
339
|
+
_need(sum(len(o['evidence']) for o in outcomes) <= MAX_REPORT_EVIDENCE,
|
|
340
|
+
'REPORT_EVIDENCE_LIMIT', 'report.outcomes')
|
|
332
341
|
# One snapshot cannot contain conflicting identities for the same source
|
|
333
342
|
# and method, including citations in separate categories. Different named
|
|
334
343
|
# digest methods remain deliberately incomparable.
|
|
@@ -347,3 +356,552 @@ def decode_report(payload, *, expected):
|
|
|
347
356
|
size = len(json.dumps(result, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8'))
|
|
348
357
|
_need(size <= MAX_REPORT_BYTES, 'REPORT_TOO_LARGE', 'report')
|
|
349
358
|
return result
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
MAX_SCOUT_ATTEMPTS = 24
|
|
362
|
+
MAX_SUBDIVISION_DEPTH = 3
|
|
363
|
+
MAX_SUBDIVISION_FANOUT = 4
|
|
364
|
+
MAX_JOIN_BYTES = 48 * 1024
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _scout_run(run):
|
|
368
|
+
"""Validate finite assignments; paths are preflight coverage atoms, not guessed files."""
|
|
369
|
+
_object(run, ('mode', 'snapshot_id', 'deadline_ms', 'budgets', 'assignments'), 'run')
|
|
370
|
+
mode = _enum(run['mode'], ('full', 'scoped'), 'run.mode')
|
|
371
|
+
snapshot = _digest(run['snapshot_id'], 64, 'run.snapshot_id')
|
|
372
|
+
deadline = run['deadline_ms']
|
|
373
|
+
_need(type(deadline) is int and deadline > 0, 'RUN_DEADLINE', 'run.deadline_ms')
|
|
374
|
+
budgets = _object(run['budgets'], ('time_limit_ms', 'token_limit', 'tool_limit'),
|
|
375
|
+
'run.budgets')
|
|
376
|
+
for name, value in budgets.items():
|
|
377
|
+
_need(type(value) is int and value > 0, 'BUDGET_REQUIRED', 'run.budgets.'+name)
|
|
378
|
+
entries = _list(run['assignments'], MAX_SCOUT_ATTEMPTS, 'run.assignments')
|
|
379
|
+
_need(bool(entries), 'CATEGORY_COVERAGE', 'run.assignments')
|
|
380
|
+
assignments = {}
|
|
381
|
+
children = {}
|
|
382
|
+
for index, item in enumerate(entries):
|
|
383
|
+
field = 'run.assignments['+str(index)+']'
|
|
384
|
+
_need(type(item) is dict, 'OBJECT_REQUIRED', field)
|
|
385
|
+
_need(not set(item) - {'assignment_id', 'parent_id', 'scope', 'overlap_reason'},
|
|
386
|
+
'UNKNOWN_FIELD', field)
|
|
387
|
+
_need({'assignment_id', 'parent_id', 'scope'} <= set(item),
|
|
388
|
+
'MISSING_FIELD', field)
|
|
389
|
+
assignment_id = _id(item['assignment_id'], field+'.assignment_id')
|
|
390
|
+
_need(assignment_id not in assignments, 'DUPLICATE_ID', field+'.assignment_id')
|
|
391
|
+
parent_id = item['parent_id']
|
|
392
|
+
if parent_id is not None:
|
|
393
|
+
parent_id = _id(parent_id, field+'.parent_id')
|
|
394
|
+
_need(parent_id in assignments, 'PARENT_REQUIRED', field+'.parent_id')
|
|
395
|
+
scope = _scope(item['scope'], field+'.scope')
|
|
396
|
+
overlap_reason = item.get('overlap_reason')
|
|
397
|
+
if overlap_reason is not None:
|
|
398
|
+
_need(parent_id is not None, 'OVERLAPPING_SCOPE', field+'.overlap_reason')
|
|
399
|
+
overlap_reason = _text(overlap_reason, 128, field+'.overlap_reason')
|
|
400
|
+
depth = 0 if parent_id is None else assignments[parent_id]['depth'] + 1
|
|
401
|
+
_need(depth <= MAX_SUBDIVISION_DEPTH, 'SUBDIVISION_DEPTH', field)
|
|
402
|
+
assignments[assignment_id] = {'assignment_id': assignment_id,
|
|
403
|
+
'parent_id': parent_id,
|
|
404
|
+
'scope': scope, 'depth': depth,
|
|
405
|
+
'overlap_reason': overlap_reason}
|
|
406
|
+
if parent_id is not None:
|
|
407
|
+
children.setdefault(parent_id, []).append(assignment_id)
|
|
408
|
+
roots = [item for item in assignments.values() if item['parent_id'] is None]
|
|
409
|
+
categories = [item['scope']['categories'][0] for item in roots
|
|
410
|
+
if len(item['scope']['categories']) == 1]
|
|
411
|
+
_need(len(categories) == len(roots) and len(set(categories)) == len(categories),
|
|
412
|
+
'CATEGORY_COVERAGE', 'run.assignments')
|
|
413
|
+
if mode == 'full':
|
|
414
|
+
_need(set(categories) == CATEGORIES, 'CATEGORY_COVERAGE', 'run.assignments')
|
|
415
|
+
for parent_id, ids in children.items():
|
|
416
|
+
field = 'run.assignments.'+parent_id
|
|
417
|
+
_need(2 <= len(ids) <= MAX_SUBDIVISION_FANOUT, 'SUBDIVISION_FANOUT', field)
|
|
418
|
+
parent = assignments[parent_id]
|
|
419
|
+
covered = {}
|
|
420
|
+
for child_id in ids:
|
|
421
|
+
child = assignments[child_id]
|
|
422
|
+
_need(child['scope']['categories'] == parent['scope']['categories'],
|
|
423
|
+
'SUBDIVISION_COVERAGE', field)
|
|
424
|
+
paths = set(child['scope']['paths'])
|
|
425
|
+
for path in paths:
|
|
426
|
+
if path in covered:
|
|
427
|
+
_need(child['overlap_reason'] is not None
|
|
428
|
+
and child['overlap_reason'] == covered[path],
|
|
429
|
+
'OVERLAPPING_SCOPE', field)
|
|
430
|
+
covered[path] = child['overlap_reason']
|
|
431
|
+
_need(set(covered) == set(parent['scope']['paths']),
|
|
432
|
+
'SUBDIVISION_COVERAGE', field)
|
|
433
|
+
return mode, snapshot, deadline, assignments, children, sorted(categories)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _scout_attempts(attempts, assignments):
|
|
437
|
+
items = _list(attempts, MAX_SCOUT_ATTEMPTS, 'attempts')
|
|
438
|
+
counts = {}
|
|
439
|
+
seen = set()
|
|
440
|
+
normalized = []
|
|
441
|
+
for index, item in enumerate(items):
|
|
442
|
+
field = 'attempts['+str(index)+']'
|
|
443
|
+
_object(item, ('assignment_id', 'attempt_id'), field)
|
|
444
|
+
assignment_id = _id(item['assignment_id'], field+'.assignment_id')
|
|
445
|
+
attempt_id = _id(item['attempt_id'], field+'.attempt_id')
|
|
446
|
+
_need(assignment_id in assignments, 'UNKNOWN_ASSIGNMENT', field)
|
|
447
|
+
_need(attempt_id not in seen, 'DUPLICATE_ID', field+'.attempt_id')
|
|
448
|
+
seen.add(attempt_id)
|
|
449
|
+
counts[assignment_id] = counts.get(assignment_id, 0) + 1
|
|
450
|
+
_need(counts[assignment_id] <= 2, 'RETRY_LIMIT', field)
|
|
451
|
+
parent_id = assignments[assignment_id]['parent_id']
|
|
452
|
+
if parent_id is not None:
|
|
453
|
+
_need(counts.get(parent_id, 0) > 0, 'PARENT_REQUIRED', field)
|
|
454
|
+
normalized.append({'assignment_id': assignment_id, 'attempt_id': attempt_id})
|
|
455
|
+
return normalized, counts
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def admit_scout_attempt(run, attempts, candidate, *, now_ms):
|
|
459
|
+
"""Admit one planned launch against the run's shared deadline and attempt ledger.
|
|
460
|
+
|
|
461
|
+
The host still owns containment, concurrency, actual dispatch, and spending.
|
|
462
|
+
Every launched attempt, including failed and subdivided work, stays in the
|
|
463
|
+
caller's ledger. Changing an assignment ID cannot replenish the aggregate.
|
|
464
|
+
"""
|
|
465
|
+
_, _, deadline, assignments, _, _ = _scout_run(run)
|
|
466
|
+
_need(type(now_ms) is int and 0 <= now_ms < deadline, 'RUN_DEADLINE', 'now_ms')
|
|
467
|
+
_need(type(attempts) is list, 'ARRAY_REQUIRED', 'attempts')
|
|
468
|
+
_need(len(attempts) < MAX_SCOUT_ATTEMPTS, 'ATTEMPT_LIMIT', 'attempts')
|
|
469
|
+
_scout_attempts(attempts + [candidate], assignments)
|
|
470
|
+
return {'assignment_id': candidate['assignment_id'],
|
|
471
|
+
'attempt_id': candidate['attempt_id']}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def join_scout_reports(run, attempts, payloads, *, now_ms):
|
|
475
|
+
"""Join complete leaf transports for the declared full or affected run only."""
|
|
476
|
+
mode, snapshot, deadline, assignments, children, categories = _scout_run(run)
|
|
477
|
+
_need(type(now_ms) is int and 0 <= now_ms < deadline, 'RUN_DEADLINE', 'now_ms')
|
|
478
|
+
ledger, counts = _scout_attempts(attempts, assignments)
|
|
479
|
+
_need(type(payloads) is dict, 'OBJECT_REQUIRED', 'payloads')
|
|
480
|
+
_need(set(payloads) == {item['attempt_id'] for item in ledger},
|
|
481
|
+
'MISSING_REPORT', 'payloads')
|
|
482
|
+
decoded = {}
|
|
483
|
+
for item in ledger:
|
|
484
|
+
assignment = assignments[item['assignment_id']]
|
|
485
|
+
report = decode_report(payloads[item['attempt_id']], expected={
|
|
486
|
+
'assignment_id': item['assignment_id'],
|
|
487
|
+
'attempt_id': item['attempt_id'],
|
|
488
|
+
'snapshot_id': snapshot,
|
|
489
|
+
'scope': assignment['scope']})
|
|
490
|
+
decoded[item['attempt_id']] = report
|
|
491
|
+
leaves = [item for item in assignments.values()
|
|
492
|
+
if item['assignment_id'] not in children]
|
|
493
|
+
selected = []
|
|
494
|
+
for assignment in leaves:
|
|
495
|
+
matching = [item for item in ledger
|
|
496
|
+
if item['assignment_id'] == assignment['assignment_id']]
|
|
497
|
+
_need(bool(matching), 'MISSING_REPORT', assignment['assignment_id'])
|
|
498
|
+
report = decoded[matching[-1]['attempt_id']]
|
|
499
|
+
_need(report['transport'] == 'complete', 'INCOMPLETE_TRANSPORT',
|
|
500
|
+
assignment['assignment_id'])
|
|
501
|
+
selected.append({**report, 'parent_id': assignment['parent_id']})
|
|
502
|
+
for parent_id in children:
|
|
503
|
+
_need(counts.get(parent_id, 0) > 0, 'MISSING_REPORT', parent_id)
|
|
504
|
+
for item in ledger:
|
|
505
|
+
if item['assignment_id'] == parent_id:
|
|
506
|
+
_need(decoded[item['attempt_id']]['transport'] != 'complete',
|
|
507
|
+
'SUBDIVISION_AFTER_COMPLETE', parent_id)
|
|
508
|
+
selected.sort(key=lambda item: item['assignment_id'])
|
|
509
|
+
source_ids = {}
|
|
510
|
+
overlap_status = {}
|
|
511
|
+
overlap_facts = {}
|
|
512
|
+
for report in selected:
|
|
513
|
+
for path in report['scope']['paths']:
|
|
514
|
+
for outcome in report['outcomes']:
|
|
515
|
+
key = (path.casefold(), outcome['category'])
|
|
516
|
+
prior = overlap_status.setdefault(key, outcome['status'])
|
|
517
|
+
_need(prior == outcome['status'], 'CONTRADICTORY_OVERLAP',
|
|
518
|
+
report['assignment_id'])
|
|
519
|
+
for outcome in report['outcomes']:
|
|
520
|
+
evidence_paths = {item['id']: item['path'].casefold()
|
|
521
|
+
for item in outcome['evidence']}
|
|
522
|
+
for finding in outcome['findings']:
|
|
523
|
+
for evidence_id in finding['evidence_refs']:
|
|
524
|
+
key = (evidence_paths[evidence_id], outcome['category'],
|
|
525
|
+
finding['name'])
|
|
526
|
+
prior = overlap_facts.setdefault(key, finding['value'])
|
|
527
|
+
_need(prior == finding['value'], 'CONTRADICTORY_OVERLAP',
|
|
528
|
+
report['assignment_id'])
|
|
529
|
+
for evidence in outcome['evidence']:
|
|
530
|
+
key = (evidence['path'].casefold(), evidence['digest_method'])
|
|
531
|
+
prior = source_ids.setdefault(key, evidence['digest'])
|
|
532
|
+
_need(prior == evidence['digest'], 'CONTRADICTORY_OVERLAP',
|
|
533
|
+
report['assignment_id'])
|
|
534
|
+
gaps = sorted({outcome['category'] for report in selected
|
|
535
|
+
for outcome in report['outcomes']
|
|
536
|
+
if outcome['status'] in ('ambiguous', 'not_inspected', 'failed', 'oversize')})
|
|
537
|
+
result = {'mode': mode, 'coverage': categories, 'full_coverage': mode == 'full',
|
|
538
|
+
'attempt_count': len(ledger), 'reports': selected, 'gaps': gaps}
|
|
539
|
+
try:
|
|
540
|
+
size = len(json.dumps(result, ensure_ascii=False, sort_keys=True,
|
|
541
|
+
separators=(',', ':')).encode('utf-8'))
|
|
542
|
+
except UnicodeError:
|
|
543
|
+
raise ReportError('INVALID_UNICODE', 'join') from None
|
|
544
|
+
_need(size <= MAX_JOIN_BYTES, 'JOIN_TOO_LARGE', 'join')
|
|
545
|
+
return result
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
_CONTEXT_EVENT_KINDS = ('attempt', 'retry', 'rescout', 'join',
|
|
549
|
+
'transient_write', 'native_write')
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def summarize_context_events(events, *, run_id, launched_attempt_ids,
|
|
553
|
+
allowed_effect_paths):
|
|
554
|
+
"""Check a bounded, caller-supplied history of context operations.
|
|
555
|
+
|
|
556
|
+
The returned counts describe supplied records only. This does not observe
|
|
557
|
+
a host, authenticate an inventory, or authorize dispatch or publication.
|
|
558
|
+
Callers must retain all prior events when resuming after a budget stop.
|
|
559
|
+
"""
|
|
560
|
+
run_id = _id(run_id, 'run_id')
|
|
561
|
+
launches = [_id(value, 'launched_attempt_ids') for value in
|
|
562
|
+
_list(launched_attempt_ids, 128, 'launched_attempt_ids')]
|
|
563
|
+
_unique(launches, 'launched_attempt_ids')
|
|
564
|
+
effects = set(_paths(allowed_effect_paths, 'allowed_effect_paths'))
|
|
565
|
+
records = _list(events, 256, 'events')
|
|
566
|
+
seen_events = set()
|
|
567
|
+
attempts = {}
|
|
568
|
+
joined = set()
|
|
569
|
+
assignment_last = {}
|
|
570
|
+
counts = {kind: 0 for kind in _CONTEXT_EVENT_KINDS}
|
|
571
|
+
written = set()
|
|
572
|
+
for index, event in enumerate(records):
|
|
573
|
+
field = 'events[' + str(index) + ']'
|
|
574
|
+
_object(event, ('event_id', 'kind', 'run_id', 'snapshot_id', 'scope',
|
|
575
|
+
'assignment_id', 'attempt_id', 'related_attempt_ids', 'path'), field)
|
|
576
|
+
event_id = _id(event['event_id'], field+'.event_id')
|
|
577
|
+
_need(event_id not in seen_events, 'DUPLICATE_ID', field+'.event_id')
|
|
578
|
+
seen_events.add(event_id)
|
|
579
|
+
kind = _enum(event['kind'], _CONTEXT_EVENT_KINDS, field+'.kind')
|
|
580
|
+
_need(event['run_id'] == run_id, 'RUN_MISMATCH', field+'.run_id')
|
|
581
|
+
snapshot = _digest(event['snapshot_id'], 64, field+'.snapshot_id')
|
|
582
|
+
scope = _scope(event['scope'], field+'.scope')
|
|
583
|
+
assignment = event['assignment_id']
|
|
584
|
+
attempt = event['attempt_id']
|
|
585
|
+
related = [_id(value, field+'.related_attempt_ids') for value in
|
|
586
|
+
_list(event['related_attempt_ids'], 128,
|
|
587
|
+
field+'.related_attempt_ids')]
|
|
588
|
+
_unique(related, field+'.related_attempt_ids')
|
|
589
|
+
path = event['path']
|
|
590
|
+
if kind in ('attempt', 'retry', 'rescout'):
|
|
591
|
+
assignment = _id(assignment, field+'.assignment_id')
|
|
592
|
+
attempt = _id(attempt, field+'.attempt_id')
|
|
593
|
+
_need(attempt in launches and attempt not in attempts,
|
|
594
|
+
'ATTEMPT_INVENTORY', field+'.attempt_id')
|
|
595
|
+
_need(not related and path is None, 'EVENT_CONTEXT', field)
|
|
596
|
+
previous = assignment_last.get(assignment)
|
|
597
|
+
if kind == 'attempt':
|
|
598
|
+
_need(previous is None, 'RETRY_REQUIRED', field)
|
|
599
|
+
elif kind == 'retry':
|
|
600
|
+
_need(previous == (snapshot, scope), 'RETRY_CONTEXT', field)
|
|
601
|
+
else:
|
|
602
|
+
_need(previous is not None and previous[0] != snapshot,
|
|
603
|
+
'RESCOUT_CONTEXT', field)
|
|
604
|
+
assignment_last[assignment] = (snapshot, scope)
|
|
605
|
+
attempts[attempt] = (snapshot, scope)
|
|
606
|
+
elif kind == 'join':
|
|
607
|
+
_need(assignment is None and attempt is None and path is None and related,
|
|
608
|
+
'EVENT_CONTEXT', field)
|
|
609
|
+
_need(all(attempt_id in attempts and attempts[attempt_id][0] == snapshot
|
|
610
|
+
and set(attempts[attempt_id][1]['categories']) <=
|
|
611
|
+
set(scope['categories'])
|
|
612
|
+
and all(any(_within(path, parent) for parent in scope['paths'])
|
|
613
|
+
for path in attempts[attempt_id][1]['paths'])
|
|
614
|
+
for attempt_id in related),
|
|
615
|
+
'JOIN_CONTEXT', field+'.related_attempt_ids')
|
|
616
|
+
joined.update(related)
|
|
617
|
+
else:
|
|
618
|
+
_need(assignment is None and not related and type(path) is str,
|
|
619
|
+
'EVENT_CONTEXT', field)
|
|
620
|
+
attempt = _id(attempt, field+'.attempt_id')
|
|
621
|
+
_need(attempt in attempts and attempts[attempt] == (snapshot, scope),
|
|
622
|
+
'WRITE_CONTEXT', field+'.attempt_id')
|
|
623
|
+
path = _path(path, field+'.path')
|
|
624
|
+
_need(path in effects, 'EFFECT_OUT_OF_SCOPE', field+'.path')
|
|
625
|
+
written.add(path)
|
|
626
|
+
counts[kind] += 1
|
|
627
|
+
_need(set(attempts) == set(launches), 'MISSING_ATTEMPT_EVENT', 'events')
|
|
628
|
+
_need(joined == set(attempts), 'MISSING_JOIN', 'events')
|
|
629
|
+
return {'evidence_basis': 'supplied_events', 'counts': counts,
|
|
630
|
+
'covered_attempt_ids': sorted(attempts),
|
|
631
|
+
'joined_attempt_ids': sorted(joined), 'effect_paths': sorted(written)}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
_COMMAND_STATES = frozenset({'declared', 'passed', 'failed', 'unknown'})
|
|
635
|
+
_SHELL_OPERATORS = frozenset({'|', '||', '&&', ';', '<', '>', '>>', '2>', '2>>'})
|
|
636
|
+
_SHELL_LAUNCHERS = frozenset({'cmd', 'cmd.exe', 'powershell', 'powershell.exe',
|
|
637
|
+
'pwsh', 'pwsh.exe', 'sh', 'sh.exe', 'bash',
|
|
638
|
+
'bash.exe', 'zsh', 'zsh.exe', 'env', 'env.exe'})
|
|
639
|
+
_CONSTRAINT_KINDS = frozenset({'existing_applicable', 'observed_upstream',
|
|
640
|
+
'proposed_adoption'})
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _known_ids(values, field):
|
|
644
|
+
_need(type(values) in (set, frozenset, list, tuple), 'ARRAY_REQUIRED', field)
|
|
645
|
+
_need(len(values) <= MAX_EVIDENCE, 'ITEM_LIMIT', field)
|
|
646
|
+
ids = [_id(value, field) for value in values]
|
|
647
|
+
_unique(ids, field)
|
|
648
|
+
return set(ids)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _record_refs(value, known, field):
|
|
652
|
+
refs = [_id(item, field) for item in _list(value, MAX_EVIDENCE, field)]
|
|
653
|
+
_unique(refs, field)
|
|
654
|
+
_need(bool(refs) and set(refs) <= known, 'DANGLING_EVIDENCE', field)
|
|
655
|
+
return sorted(refs)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _verification_binding(value, field):
|
|
659
|
+
_object(value, ('source_snapshot_id', 'runtime_ref', 'lock_digest',
|
|
660
|
+
'prerequisites_digest', 'oracle_ref', 'argv', 'cwd',
|
|
661
|
+
'declared_prerequisites', 'environment_digest'), field)
|
|
662
|
+
argv = [_text(arg, 512, field+'.argv')
|
|
663
|
+
for arg in _list(value['argv'], 32, field+'.argv')]
|
|
664
|
+
_need(bool(argv), 'EMPTY_ARGV', field+'.argv')
|
|
665
|
+
declared_prerequisites = [
|
|
666
|
+
_text(item, 128, field+'.declared_prerequisites')
|
|
667
|
+
for item in _list(value['declared_prerequisites'], 16,
|
|
668
|
+
field+'.declared_prerequisites')
|
|
669
|
+
]
|
|
670
|
+
_unique(declared_prerequisites, field+'.declared_prerequisites')
|
|
671
|
+
return {
|
|
672
|
+
'source_snapshot_id': _digest(value['source_snapshot_id'], 64,
|
|
673
|
+
field+'.source_snapshot_id'),
|
|
674
|
+
'runtime_ref': _text(value['runtime_ref'], 128, field+'.runtime_ref'),
|
|
675
|
+
'lock_digest': _digest(value['lock_digest'], 64, field+'.lock_digest'),
|
|
676
|
+
'prerequisites_digest': _digest(value['prerequisites_digest'], 64,
|
|
677
|
+
field+'.prerequisites_digest'),
|
|
678
|
+
'oracle_ref': _text(value['oracle_ref'], 128, field+'.oracle_ref'),
|
|
679
|
+
'argv': argv,
|
|
680
|
+
'cwd': _path(value['cwd'], field+'.cwd', root=True),
|
|
681
|
+
'declared_prerequisites': declared_prerequisites,
|
|
682
|
+
# The caller supplies a digest of a non-secret environment identity,
|
|
683
|
+
# never raw variable values or a credential-bearing environment dump.
|
|
684
|
+
'environment_digest': _digest(value['environment_digest'], 64,
|
|
685
|
+
field+'.environment_digest'),
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def validate_command_record(value, *, evidence_ids, observation_ids,
|
|
690
|
+
current_binding=None):
|
|
691
|
+
"""Normalize one inert command claim; supplied IDs/bindings do not prove execution.
|
|
692
|
+
|
|
693
|
+
The caller owns observation provenance and authorization. A matching supplied
|
|
694
|
+
binding exposes only a reported current state, never a permission to execute.
|
|
695
|
+
"""
|
|
696
|
+
known_evidence = _known_ids(evidence_ids, 'evidence_ids')
|
|
697
|
+
known_observations = _known_ids(observation_ids, 'observation_ids')
|
|
698
|
+
_object(value, ('id', 'invocation', 'cwd', 'prerequisites', 'evidence_refs',
|
|
699
|
+
'verification'), 'command')
|
|
700
|
+
invocation = value['invocation']
|
|
701
|
+
_need(type(invocation) is dict, 'OBJECT_REQUIRED', 'command.invocation')
|
|
702
|
+
kind = _enum(invocation.get('kind'), ('literal_argv', 'declaration_ref'),
|
|
703
|
+
'command.invocation.kind')
|
|
704
|
+
if kind == 'literal_argv':
|
|
705
|
+
_object(invocation, ('kind', 'argv'), 'command.invocation')
|
|
706
|
+
argv = [_text(arg, 512, 'command.invocation.argv')
|
|
707
|
+
for arg in _list(invocation['argv'], 32, 'command.invocation.argv')]
|
|
708
|
+
_need(bool(argv), 'EMPTY_ARGV', 'command.invocation.argv')
|
|
709
|
+
executable = argv[0]
|
|
710
|
+
basename = executable.replace('\\', '/').rsplit('/', 1)[-1].casefold()
|
|
711
|
+
first_space = next((i for i, char in enumerate(executable)
|
|
712
|
+
if char.isspace()), len(executable))
|
|
713
|
+
first_separator = min((i for i, char in enumerate(executable)
|
|
714
|
+
if char in '/\\'), default=len(executable))
|
|
715
|
+
_need(executable == executable.strip()
|
|
716
|
+
and not executable.startswith(('"', "'"))
|
|
717
|
+
and (first_space == len(executable) or first_separator < first_space)
|
|
718
|
+
and not re.match(r'[A-Za-z_][A-Za-z0-9_]*=', executable)
|
|
719
|
+
and not any(char in executable for char in '|&;<>')
|
|
720
|
+
and basename not in _SHELL_LAUNCHERS
|
|
721
|
+
and not basename.endswith(('.cmd', '.bat'))
|
|
722
|
+
and not any(arg in _SHELL_OPERATORS for arg in argv),
|
|
723
|
+
'DECLARATION_REQUIRED', 'command.invocation.argv')
|
|
724
|
+
normalized_invocation = {'kind': kind, 'argv': argv}
|
|
725
|
+
else:
|
|
726
|
+
_object(invocation, ('kind', 'path', 'selector', 'text'), 'command.invocation')
|
|
727
|
+
normalized_invocation = {
|
|
728
|
+
'kind': kind,
|
|
729
|
+
'path': _path(invocation['path'], 'command.invocation.path'),
|
|
730
|
+
'selector': _text(invocation['selector'], 128, 'command.invocation.selector'),
|
|
731
|
+
'text': _text(invocation['text'], 512, 'command.invocation.text'),
|
|
732
|
+
}
|
|
733
|
+
cwd = _path(value['cwd'], 'command.cwd', root=True)
|
|
734
|
+
prerequisites = [_text(item, 128, 'command.prerequisites')
|
|
735
|
+
for item in _list(value['prerequisites'], 16,
|
|
736
|
+
'command.prerequisites')]
|
|
737
|
+
_unique(prerequisites, 'command.prerequisites')
|
|
738
|
+
refs = _record_refs(value['evidence_refs'], known_evidence,
|
|
739
|
+
'command.evidence_refs')
|
|
740
|
+
verification = value['verification']
|
|
741
|
+
_object(verification, ('state', 'observation_ref', 'binding'),
|
|
742
|
+
'command.verification')
|
|
743
|
+
state = _enum(verification['state'], _COMMAND_STATES,
|
|
744
|
+
'command.verification.state')
|
|
745
|
+
observation_ref = verification['observation_ref']
|
|
746
|
+
binding = verification['binding']
|
|
747
|
+
if state in ('passed', 'failed'):
|
|
748
|
+
_need(kind == 'literal_argv', 'DECLARATION_UNVERIFIED',
|
|
749
|
+
'command.verification')
|
|
750
|
+
observation_ref = _id(observation_ref, 'command.verification.observation_ref')
|
|
751
|
+
_need(observation_ref in known_observations, 'UNKNOWN_OBSERVATION',
|
|
752
|
+
'command.verification.observation_ref')
|
|
753
|
+
binding = _verification_binding(binding, 'command.verification.binding')
|
|
754
|
+
else:
|
|
755
|
+
_need(observation_ref is None and binding is None, 'UNSUPPORTED_VERIFICATION',
|
|
756
|
+
'command.verification')
|
|
757
|
+
current = (_verification_binding(current_binding, 'current_binding')
|
|
758
|
+
if current_binding is not None else None)
|
|
759
|
+
same_command = (kind == 'literal_argv' and binding is not None
|
|
760
|
+
and binding['argv'] == normalized_invocation['argv']
|
|
761
|
+
and binding['cwd'] == cwd
|
|
762
|
+
and binding['declared_prerequisites'] == prerequisites)
|
|
763
|
+
reported_current_state = (state if state in ('declared', 'unknown') or
|
|
764
|
+
(same_command and binding == current) else 'unknown')
|
|
765
|
+
return {
|
|
766
|
+
'id': _id(value['id'], 'command.id'),
|
|
767
|
+
'invocation': normalized_invocation,
|
|
768
|
+
'cwd': cwd,
|
|
769
|
+
'prerequisites': prerequisites,
|
|
770
|
+
'evidence_refs': refs,
|
|
771
|
+
'verification': {'state': state, 'observation_ref': observation_ref,
|
|
772
|
+
'binding': binding},
|
|
773
|
+
'reported_current_state': reported_current_state,
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def resolve_scoped_command_guidance(entries, *, command_id, cwd,
|
|
778
|
+
current_binding, observation):
|
|
779
|
+
"""Resolve one command by ID and cwd, retaining its source and observed result.
|
|
780
|
+
|
|
781
|
+
``observation`` is supplied by the owning execution workflow. This pure
|
|
782
|
+
consumer checks its shape and result, but cannot attest its provenance.
|
|
783
|
+
A passed report is therefore only a reported state for that supplied run.
|
|
784
|
+
"""
|
|
785
|
+
command_id = _id(command_id, 'command_id')
|
|
786
|
+
cwd = _path(cwd, 'cwd', root=True)
|
|
787
|
+
current = (_verification_binding(current_binding, 'current_binding')
|
|
788
|
+
if current_binding is not None else None)
|
|
789
|
+
_need(type(entries) is list and 0 < len(entries) <= MAX_EVIDENCE,
|
|
790
|
+
'ITEM_LIMIT', 'entries')
|
|
791
|
+
matches = []
|
|
792
|
+
for index, entry in enumerate(entries):
|
|
793
|
+
field = 'entries['+str(index)+']'
|
|
794
|
+
_need(type(entry) is tuple and len(entry) == 2,
|
|
795
|
+
'OBJECT_REQUIRED', field)
|
|
796
|
+
record, sources = entry
|
|
797
|
+
_need(type(sources) is dict and 0 < len(sources) <= MAX_EVIDENCE,
|
|
798
|
+
'OBJECT_REQUIRED', field+'.sources')
|
|
799
|
+
normalized_sources = {}
|
|
800
|
+
for evidence_id, source in sources.items():
|
|
801
|
+
evidence_id = _id(evidence_id, field+'.sources.id')
|
|
802
|
+
_object(source, ('path', 'digest'), field+'.sources.'+evidence_id)
|
|
803
|
+
normalized_sources[evidence_id] = {
|
|
804
|
+
'path': _path(source['path'], field+'.sources.path'),
|
|
805
|
+
'digest': _digest(source['digest'], 64, field+'.sources.digest'),
|
|
806
|
+
}
|
|
807
|
+
# A stored observation reference is accepted for shape normalization
|
|
808
|
+
# only. This resolver never treats that self-reported history as fresh.
|
|
809
|
+
historical_ref = (record.get('verification', {}).get('observation_ref')
|
|
810
|
+
if type(record) is dict and
|
|
811
|
+
type(record.get('verification')) is dict else None)
|
|
812
|
+
normalized = validate_command_record(
|
|
813
|
+
record, evidence_ids=set(normalized_sources),
|
|
814
|
+
observation_ids={historical_ref} if type(historical_ref) is str else set(),
|
|
815
|
+
current_binding=None)
|
|
816
|
+
if normalized['id'] == command_id and normalized['cwd'] == cwd:
|
|
817
|
+
matches.append((normalized, normalized_sources))
|
|
818
|
+
_need(len(matches) == 1, 'AMBIGUOUS_COMMAND', 'entries')
|
|
819
|
+
command, sources = matches[0]
|
|
820
|
+
_need(len(command['evidence_refs']) == 1, 'AMBIGUOUS_SOURCE', 'command.evidence_refs')
|
|
821
|
+
source = sources[command['evidence_refs'][0]]
|
|
822
|
+
source_path = source['path']
|
|
823
|
+
if source_path.rsplit('/', 1)[-1].casefold() == 'package.json':
|
|
824
|
+
_need(cwd == source_path.rsplit('/', 1)[0] if '/' in source_path else cwd == '.',
|
|
825
|
+
'SOURCE_CWD_MISMATCH', 'command.cwd')
|
|
826
|
+
if command['invocation']['kind'] == 'declaration_ref':
|
|
827
|
+
_need(command['invocation']['path'] == source_path,
|
|
828
|
+
'SOURCE_MISMATCH', 'command.invocation.path')
|
|
829
|
+
result = {**command, 'source': source, 'observation': None}
|
|
830
|
+
if observation is None:
|
|
831
|
+
return result
|
|
832
|
+
_object(observation, ('id', 'authorized', 'binding', 'source', 'exit', 'collector',
|
|
833
|
+
'required_tests', 'tests'), 'observation')
|
|
834
|
+
observed_id = _id(observation['id'], 'observation.id')
|
|
835
|
+
_need(type(observation['authorized']) is bool,
|
|
836
|
+
'BOOLEAN_REQUIRED', 'observation.authorized')
|
|
837
|
+
observed_binding = _verification_binding(observation['binding'],
|
|
838
|
+
'observation.binding')
|
|
839
|
+
_object(observation['source'], ('path', 'digest'), 'observation.source')
|
|
840
|
+
observed_source = {
|
|
841
|
+
'path': _path(observation['source']['path'], 'observation.source.path'),
|
|
842
|
+
'digest': _digest(observation['source']['digest'], 64,
|
|
843
|
+
'observation.source.digest'),
|
|
844
|
+
}
|
|
845
|
+
exit_code = observation['exit']
|
|
846
|
+
_need(type(exit_code) is int and 0 <= exit_code <= 255,
|
|
847
|
+
'INVALID_EXIT', 'observation.exit')
|
|
848
|
+
collector = _enum(observation['collector'],
|
|
849
|
+
('python-unittest-text/0.1.0', 'codearbiter-named-lines/0.1.0',
|
|
850
|
+
'vitest-verbose/0.1.0', 'playwright-json/0.1.0',
|
|
851
|
+
'exit-only/0.1.0'), 'observation.collector')
|
|
852
|
+
required = [_text(item, 128, 'observation.required_tests') for item in
|
|
853
|
+
_list(observation['required_tests'], MAX_EVIDENCE,
|
|
854
|
+
'observation.required_tests')]
|
|
855
|
+
_unique(required, 'observation.required_tests')
|
|
856
|
+
tests = []
|
|
857
|
+
for test in _list(observation['tests'], MAX_EVIDENCE, 'observation.tests'):
|
|
858
|
+
_object(test, ('name', 'status'), 'observation.tests')
|
|
859
|
+
tests.append({'name': _text(test['name'], 128, 'observation.tests.name'),
|
|
860
|
+
'status': _enum(test['status'], ('pass', 'fail', 'skip'),
|
|
861
|
+
'observation.tests.status')})
|
|
862
|
+
names = [test['name'] for test in tests]
|
|
863
|
+
unique = len(set(names)) == len(names)
|
|
864
|
+
named_oracle = 'sha256:' + hashlib.sha256(json.dumps(
|
|
865
|
+
{'collector': collector, 'required_tests': sorted(required)},
|
|
866
|
+
sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()
|
|
867
|
+
matches_oracle = (collector != 'exit-only/0.1.0' and bool(required)
|
|
868
|
+
and observed_binding['oracle_ref'] == named_oracle
|
|
869
|
+
and unique and set(names) == set(required)
|
|
870
|
+
and all(test['status'] == 'pass' for test in tests))
|
|
871
|
+
exit_oracle = (collector == 'exit-only/0.1.0' and not required and not tests
|
|
872
|
+
and observed_binding['oracle_ref'] == 'exit-only/0.1.0')
|
|
873
|
+
same_invocation = (command['invocation']['kind'] == 'literal_argv'
|
|
874
|
+
and observed_binding['argv'] == command['invocation']['argv']
|
|
875
|
+
and observed_binding['cwd'] == cwd
|
|
876
|
+
and observed_binding['declared_prerequisites'] ==
|
|
877
|
+
command['prerequisites'])
|
|
878
|
+
current_run = (observation['authorized'] and same_invocation
|
|
879
|
+
and observed_source == source
|
|
880
|
+
and observed_binding == current)
|
|
881
|
+
state = ('passed' if current_run and exit_code == 0 and
|
|
882
|
+
(matches_oracle or exit_oracle) else
|
|
883
|
+
'failed' if current_run and (exit_code != 0 or
|
|
884
|
+
any(test['status'] == 'fail' for test in tests))
|
|
885
|
+
else 'unknown')
|
|
886
|
+
result['reported_current_state'] = state
|
|
887
|
+
result['observation'] = {'id': observed_id, 'exit': exit_code,
|
|
888
|
+
'source': observed_source,
|
|
889
|
+
'collector': collector, 'required_tests': required,
|
|
890
|
+
'tests': tests}
|
|
891
|
+
return result
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def validate_constraint_reference(value, *, evidence_ids):
|
|
895
|
+
"""Normalize a source-scoped reference without adopting or approving a rule."""
|
|
896
|
+
known_evidence = _known_ids(evidence_ids, 'evidence_ids')
|
|
897
|
+
_object(value, ('id', 'kind', 'source_ref', 'scope', 'statement',
|
|
898
|
+
'evidence_refs'), 'constraint')
|
|
899
|
+
return {
|
|
900
|
+
'id': _id(value['id'], 'constraint.id'),
|
|
901
|
+
'kind': _enum(value['kind'], _CONSTRAINT_KINDS, 'constraint.kind'),
|
|
902
|
+
'source_ref': _text(value['source_ref'], 256, 'constraint.source_ref'),
|
|
903
|
+
'scope': _path(value['scope'], 'constraint.scope', root=True),
|
|
904
|
+
'statement': _text(value['statement'], 512, 'constraint.statement'),
|
|
905
|
+
'evidence_refs': _record_refs(value['evidence_refs'], known_evidence,
|
|
906
|
+
'constraint.evidence_refs'),
|
|
907
|
+
}
|