@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.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +33 -0
  4. package/plugins/ca-pi/SKILLS.md +1 -1
  5. package/plugins/ca-pi/agents/infra-author.md +7 -1
  6. package/plugins/ca-pi/agents/scout.md +40 -8
  7. package/plugins/ca-pi/generated/command-catalog.json +1 -1
  8. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
  9. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
  10. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
  11. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
  12. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
  13. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
  14. package/plugins/ca-pi/helpers/artifacts/release.json +104 -6
  15. package/plugins/ca-pi/hooks/_activationlib.py +97 -13
  16. package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
  17. package/plugins/ca-pi/hooks/_artifactauthoritylib.py +313 -76
  18. package/plugins/ca-pi/hooks/_artifactlib.py +166 -2
  19. package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
  20. package/plugins/ca-pi/hooks/_bashguardlib.py +14 -0
  21. package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
  22. package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
  23. package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
  24. package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
  25. package/plugins/ca-pi/hooks/_hooklib.py +1 -0
  26. package/plugins/ca-pi/hooks/_host.py +1 -1
  27. package/plugins/ca-pi/hooks/_intentlib.py +5 -5
  28. package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
  29. package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
  30. package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
  31. package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
  32. package/plugins/ca-pi/hooks/artifact-authority.py +20 -1
  33. package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
  34. package/plugins/ca-pi/hooks/doctor.py +4 -2
  35. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  36. package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
  37. package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
  38. package/plugins/ca-pi/hooks/session-start.py +26 -7
  39. package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
  40. package/plugins/ca-pi/includes/helper-invocation.md +128 -0
  41. package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
  42. package/plugins/ca-pi/includes/routing-table.md +1 -1
  43. package/plugins/ca-pi/package.json +1 -1
  44. package/plugins/ca-pi/routines/INDEX.md +1 -1
  45. package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
  46. package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
  47. package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
  48. package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
  49. package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
  50. package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
  51. package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
  52. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
  53. package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
  54. package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
  55. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
  56. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
  57. package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
  58. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
  59. package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
  60. package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
  61. package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
  62. package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
  63. package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
  64. 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 six-domain coverage, containment, truth, freshness, or readiness.
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 = 48 * 1024
24
+ MAX_REPORT_BYTES = 64 * 1024
19
25
  MAX_DEPTH = 12
20
26
  MAX_NODES = 4096
21
27
  MAX_PATHS = 16
22
- MAX_EVIDENCE = 64
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, MAX_EVIDENCE, field)):
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'], MAX_EVIDENCE, part)]
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
+ }