okstra 0.179.2 → 0.183.2
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/dist/cli-registry.mjs +30 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +21 -10
- package/docs/cli.md +36 -20
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +43 -31
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +9 -12
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +10 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
- package/runtime/prompts/lead/plan-body-verification.md +217 -23
- package/runtime/prompts/lead/report-writer.md +81 -421
- package/runtime/prompts/lead/team-contract.md +9 -54
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +20 -12
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +141 -38
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
- package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
- package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
- package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +21 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +74 -16
- package/runtime/python/okstra_ctl/render_final_report.py +10 -8
- package/runtime/python/okstra_ctl/report_assembly.py +509 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +67 -32
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +81 -8
- package/runtime/python/okstra_ctl/report_narrative.py +351 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +184 -65
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +96 -28
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +3 -2
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +29 -19
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +636 -72
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""런타임 계약 그래프 검증 명령 진입점."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .contract_graph import (
|
|
10
|
+
ContractFile,
|
|
11
|
+
ContractGraphError,
|
|
12
|
+
ContractGraphRoot,
|
|
13
|
+
ContractGraphValidator,
|
|
14
|
+
ContractSelection,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parser() -> argparse.ArgumentParser:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="okstra contract-check",
|
|
21
|
+
description="Validate runtime contract documents and dependencies.",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("--root", required=True, type=Path)
|
|
24
|
+
selection = parser.add_mutually_exclusive_group()
|
|
25
|
+
selection.add_argument("--profile")
|
|
26
|
+
selection.add_argument("--operation")
|
|
27
|
+
return parser
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _record(row: ContractFile) -> dict[str, str]:
|
|
31
|
+
return {
|
|
32
|
+
"path": row.relative_path,
|
|
33
|
+
"schemaVersion": row.schema_version,
|
|
34
|
+
"sha256": row.sha256,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv: list[str] | None = None) -> int:
|
|
39
|
+
args = _parser().parse_args(argv)
|
|
40
|
+
selected = None
|
|
41
|
+
if args.profile is not None or args.operation is not None:
|
|
42
|
+
selected = ContractSelection(args.profile, args.operation)
|
|
43
|
+
try:
|
|
44
|
+
graph = ContractGraphValidator().validate(
|
|
45
|
+
ContractGraphRoot.from_base(args.root), selected=selected
|
|
46
|
+
)
|
|
47
|
+
if args.profile is not None:
|
|
48
|
+
rows = graph.dependencies_for_profile(args.profile)
|
|
49
|
+
elif args.operation is not None:
|
|
50
|
+
rows = graph.dependencies_for_operation(args.operation)
|
|
51
|
+
else:
|
|
52
|
+
rows = graph.files
|
|
53
|
+
except ContractGraphError as exc:
|
|
54
|
+
for error in exc.errors:
|
|
55
|
+
print(error, file=sys.stderr)
|
|
56
|
+
return 1
|
|
57
|
+
print(json.dumps([_record(row) for row in rows], indent=2))
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
raise SystemExit(main())
|
|
@@ -3,11 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
5
|
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
6
7
|
import json
|
|
7
8
|
from pathlib import Path, PurePosixPath
|
|
8
9
|
import sys
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
12
|
+
from okstra_project.state import StateError, parse_task_key, slugify
|
|
13
|
+
|
|
11
14
|
from .convergence_engine import (
|
|
12
15
|
ConvergenceContractError,
|
|
13
16
|
apply_critic_gap_results,
|
|
@@ -27,16 +30,107 @@ from .convergence_migration import (
|
|
|
27
30
|
from .convergence_provenance import (
|
|
28
31
|
GROUPS_BASENAME_RE,
|
|
29
32
|
provenance_errors,
|
|
33
|
+
worker_result_suffix,
|
|
30
34
|
)
|
|
31
35
|
from .convergence_store import (
|
|
32
|
-
|
|
36
|
+
load_convergence_critic_batch,
|
|
37
|
+
load_convergence_round_results,
|
|
38
|
+
load_owned_json_object,
|
|
33
39
|
write_final_state_atomic,
|
|
34
40
|
write_json_atomic,
|
|
35
41
|
)
|
|
36
42
|
from .execution_identity import ExecutionManifest
|
|
37
43
|
from .execution_manifest import read_execution_manifest_view
|
|
44
|
+
from .final_report_schema import load_named_schema, validate as validate_schema
|
|
38
45
|
from .paths import RunRef
|
|
46
|
+
from .report_finalize import task_manifest_path
|
|
39
47
|
from .verdict_blocks import FindingVote, VerdictBlockError, parse_finding_votes
|
|
48
|
+
from .fixed_text import line
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class RunArtifactAuthority:
|
|
53
|
+
manifest_path: Path
|
|
54
|
+
project_root: Path
|
|
55
|
+
run_dir: Path
|
|
56
|
+
task_type: str
|
|
57
|
+
task_key: str
|
|
58
|
+
manifest_ref: str
|
|
59
|
+
state_sequence: str
|
|
60
|
+
execution_manifest: ExecutionManifest | None
|
|
61
|
+
payload: Mapping[str, Any]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validated_run_authority(manifest_path: Path) -> RunArtifactAuthority:
|
|
65
|
+
supplied = Path(manifest_path)
|
|
66
|
+
if not supplied.is_absolute() or supplied.absolute() != supplied.resolve():
|
|
67
|
+
raise ConvergenceContractError("run manifest path is not canonical")
|
|
68
|
+
payload = load_owned_json_object(supplied)
|
|
69
|
+
execution_manifest = None
|
|
70
|
+
if payload.get("executionIdentityVersion") == 2:
|
|
71
|
+
execution_manifest, view = read_execution_manifest_view(supplied)
|
|
72
|
+
payload = dict(view)
|
|
73
|
+
project_root = _manifest_project_root(payload)
|
|
74
|
+
manifest_ref = _manifest_run_ref(payload, project_root, supplied)
|
|
75
|
+
run_dir, task_type, state_sequence = _validated_run_artifact_authority(
|
|
76
|
+
payload, project_root, supplied, manifest_ref
|
|
77
|
+
)
|
|
78
|
+
task_key = _manifest_authority_string(payload, "taskKey")
|
|
79
|
+
try:
|
|
80
|
+
_project_id, task_group, task_id = parse_task_key(task_key)
|
|
81
|
+
except StateError as exc:
|
|
82
|
+
raise ConvergenceContractError(str(exc)) from exc
|
|
83
|
+
recovered = RunRef.from_run_dir(run_dir)
|
|
84
|
+
if (slugify(task_group), slugify(task_id)) != (
|
|
85
|
+
recovered.task_group, recovered.task_id
|
|
86
|
+
):
|
|
87
|
+
raise ConvergenceContractError(
|
|
88
|
+
"run manifest taskKey does not match run directory"
|
|
89
|
+
)
|
|
90
|
+
return RunArtifactAuthority(
|
|
91
|
+
supplied, project_root, run_dir, task_type, task_key, manifest_ref,
|
|
92
|
+
state_sequence, execution_manifest, payload,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def canonical_run_state_artifact(
|
|
97
|
+
authority: RunArtifactAuthority,
|
|
98
|
+
*,
|
|
99
|
+
manifest_field: str,
|
|
100
|
+
prefix: str,
|
|
101
|
+
label: str,
|
|
102
|
+
) -> Path:
|
|
103
|
+
value = _manifest_authority_string(authority.payload, manifest_field)
|
|
104
|
+
supplied = _canonical_project_artifact_path(
|
|
105
|
+
authority.project_root, value, label
|
|
106
|
+
)
|
|
107
|
+
expected = authority.run_dir / "state" / _canonical_run_artifact_name(
|
|
108
|
+
prefix, authority.task_type, authority.state_sequence
|
|
109
|
+
)
|
|
110
|
+
if supplied != expected:
|
|
111
|
+
raise ConvergenceContractError(f"{label} does not match run authority")
|
|
112
|
+
return expected
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def canonical_run_report_artifact(authority: RunArtifactAuthority) -> Path:
|
|
116
|
+
report_sequence = _manifest_sequence(authority.payload, "reports")
|
|
117
|
+
try:
|
|
118
|
+
expected = RunRef.from_run_dir(
|
|
119
|
+
authority.run_dir, seq=int(report_sequence)
|
|
120
|
+
).report
|
|
121
|
+
except ValueError as exc:
|
|
122
|
+
raise ConvergenceContractError(
|
|
123
|
+
"run manifest report sequence is not canonical"
|
|
124
|
+
) from exc
|
|
125
|
+
value = _manifest_authority_string(
|
|
126
|
+
authority.payload, "expectedReportRecordPath"
|
|
127
|
+
)
|
|
128
|
+
supplied = _canonical_project_artifact_path(
|
|
129
|
+
authority.project_root, value, "report path"
|
|
130
|
+
)
|
|
131
|
+
if supplied != expected:
|
|
132
|
+
raise ConvergenceContractError("report path does not match run authority")
|
|
133
|
+
return expected
|
|
40
134
|
|
|
41
135
|
|
|
42
136
|
_EVIDENCE_ARTIFACT = {
|
|
@@ -197,6 +291,11 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
197
291
|
seed.add_argument("--final-state", type=Path, required=True)
|
|
198
292
|
seed.add_argument("--migration-dir", type=Path, required=True)
|
|
199
293
|
seed.add_argument("--restart-from-round0", action="store_true")
|
|
294
|
+
prepare = subparsers.add_parser(
|
|
295
|
+
"prepare-groups", help="publish grouped findings from fixed Markdown"
|
|
296
|
+
)
|
|
297
|
+
prepare.add_argument("--run-manifest", type=Path, required=True)
|
|
298
|
+
prepare.add_argument("--input", type=Path, required=True)
|
|
200
299
|
|
|
201
300
|
plan = subparsers.add_parser("plan-round", help="write the next dispatch plan")
|
|
202
301
|
plan.add_argument("--work-state", type=Path, required=True)
|
|
@@ -292,7 +391,7 @@ def _resume_after_legacy_final(
|
|
|
292
391
|
source_path=final_state_path,
|
|
293
392
|
migration_dir=migration_dir,
|
|
294
393
|
)
|
|
295
|
-
state =
|
|
394
|
+
state = load_owned_json_object(work_state_path)
|
|
296
395
|
state["migration"] = migration_record(final_state_path, archive_path, digest)
|
|
297
396
|
write_json_atomic(work_state_path, state)
|
|
298
397
|
|
|
@@ -305,16 +404,21 @@ def _seeded_groups_provenance_errors(groups_path: Path, document: dict) -> list[
|
|
|
305
404
|
file, and the same claims are re-checked at Phase 7 by `validate-run`,
|
|
306
405
|
where a bad `itemId` surfaces hours later as a blocked approval.
|
|
307
406
|
"""
|
|
308
|
-
|
|
309
|
-
if match is None:
|
|
407
|
+
if GROUPS_BASENAME_RE.match(groups_path.name) is None:
|
|
310
408
|
return []
|
|
311
|
-
|
|
409
|
+
run_dir = groups_path.parent.parent
|
|
410
|
+
worker_results_dir = run_dir / "worker-results"
|
|
312
411
|
if not worker_results_dir.is_dir():
|
|
313
412
|
return []
|
|
413
|
+
# 그룹 파일명이 지닌 것은 state 시퀀스다. 워커 결과는 workerResults 시퀀스로
|
|
414
|
+
# 이름 붙으므로 그룹 문서가 기록한 run manifest 를 거쳐 해소한다.
|
|
415
|
+
suffix = worker_result_suffix(run_dir, document)
|
|
416
|
+
if suffix is None:
|
|
417
|
+
return []
|
|
314
418
|
return provenance_errors(
|
|
315
419
|
document,
|
|
316
420
|
worker_results_dir=worker_results_dir,
|
|
317
|
-
suffix=
|
|
421
|
+
suffix=suffix,
|
|
318
422
|
groups_label=groups_path.name,
|
|
319
423
|
)
|
|
320
424
|
|
|
@@ -698,7 +802,7 @@ def _canonical_project_artifact_path(
|
|
|
698
802
|
|
|
699
803
|
|
|
700
804
|
def _seed(args: argparse.Namespace) -> tuple[str, Path]:
|
|
701
|
-
grouped_input =
|
|
805
|
+
grouped_input = load_owned_json_object(args.groups)
|
|
702
806
|
execution_manifest = _seed_execution_manifest(
|
|
703
807
|
args.groups,
|
|
704
808
|
grouped_input,
|
|
@@ -746,7 +850,7 @@ def _seed(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
746
850
|
|
|
747
851
|
|
|
748
852
|
def _plan_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
749
|
-
state =
|
|
853
|
+
state = load_owned_json_object(args.work_state)
|
|
750
854
|
errors = validate_working_state(state)
|
|
751
855
|
if errors:
|
|
752
856
|
raise ConvergenceContractError("invalid working state: " + "; ".join(errors))
|
|
@@ -756,10 +860,10 @@ def _plan_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
756
860
|
|
|
757
861
|
|
|
758
862
|
def _apply_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
759
|
-
state =
|
|
863
|
+
state = load_owned_json_object(args.work_state)
|
|
760
864
|
_validated_v2_work_authority(args.work_state, state)
|
|
761
|
-
plan =
|
|
762
|
-
results =
|
|
865
|
+
plan = load_owned_json_object(args.plan)
|
|
866
|
+
results = load_convergence_round_results(args.results)
|
|
763
867
|
updated = apply_round_results(state, plan, results)
|
|
764
868
|
errors = validate_working_state(updated)
|
|
765
869
|
if errors:
|
|
@@ -769,16 +873,16 @@ def _apply_round(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
769
873
|
|
|
770
874
|
|
|
771
875
|
def _apply_critic_gaps(args: argparse.Namespace) -> tuple[str, Path]:
|
|
772
|
-
state =
|
|
876
|
+
state = load_owned_json_object(args.work_state)
|
|
773
877
|
_validated_v2_work_authority(args.work_state, state)
|
|
774
|
-
results =
|
|
878
|
+
results = load_convergence_critic_batch(args.results)
|
|
775
879
|
updated = apply_critic_gap_results(state, results)
|
|
776
880
|
write_json_atomic(args.work_state, updated)
|
|
777
881
|
return "applied", args.work_state
|
|
778
882
|
|
|
779
883
|
|
|
780
884
|
def _finalize(args: argparse.Namespace) -> tuple[str, Path]:
|
|
781
|
-
state =
|
|
885
|
+
state = load_owned_json_object(args.work_state)
|
|
782
886
|
authority = _validated_v2_work_authority(args.work_state, state)
|
|
783
887
|
if authority is not None:
|
|
784
888
|
_manifest, run_dir, task_type, state_sequence = authority
|
|
@@ -797,7 +901,7 @@ def _finalize(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
797
901
|
|
|
798
902
|
|
|
799
903
|
def _validate(args: argparse.Namespace) -> tuple[str, Path]:
|
|
800
|
-
state =
|
|
904
|
+
state = load_owned_json_object(args.state)
|
|
801
905
|
errors = (
|
|
802
906
|
validate_working_state(state)
|
|
803
907
|
if args.kind == "working"
|
|
@@ -881,7 +985,7 @@ def _worker_votes(
|
|
|
881
985
|
|
|
882
986
|
|
|
883
987
|
def _collect_results(args: argparse.Namespace) -> tuple[str, Path]:
|
|
884
|
-
plan =
|
|
988
|
+
plan = load_owned_json_object(args.plan)
|
|
885
989
|
planned = _planned_finding_ids(plan)
|
|
886
990
|
dispatches = _dispatch_rows(args.dispatch)
|
|
887
991
|
undispatched = sorted(set(planned) - {row["worker"] for row in dispatches})
|
|
@@ -909,8 +1013,222 @@ def _collect_results(args: argparse.Namespace) -> tuple[str, Path]:
|
|
|
909
1013
|
return "collected", args.output
|
|
910
1014
|
|
|
911
1015
|
|
|
1016
|
+
def _parse_grouping_markdown(path: Path) -> list[dict[str, Any]]:
|
|
1017
|
+
groups: list[dict[str, Any]] = []
|
|
1018
|
+
fields: dict[str, list[str]] = {}
|
|
1019
|
+
finding_id = ""
|
|
1020
|
+
for raw in [*path.read_text(encoding="utf-8").splitlines(), "## END"]:
|
|
1021
|
+
if raw.startswith("## "):
|
|
1022
|
+
if finding_id:
|
|
1023
|
+
groups.append(_group_from_fields(finding_id, fields))
|
|
1024
|
+
finding_id = raw[3:].strip()
|
|
1025
|
+
fields = {}
|
|
1026
|
+
elif ":" in raw and finding_id:
|
|
1027
|
+
key, value = raw.split(":", 1)
|
|
1028
|
+
fields.setdefault(key.strip().lower(), []).append(value.strip())
|
|
1029
|
+
return groups
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _parse_group_source(spec: str) -> tuple[str, str, str]:
|
|
1033
|
+
identity, separator, evidence = spec.partition("|")
|
|
1034
|
+
worker, colon, item_id = identity.strip().partition(":")
|
|
1035
|
+
if not separator or not colon or not all((worker, item_id, evidence.strip())):
|
|
1036
|
+
raise ConvergenceContractError(
|
|
1037
|
+
"Source must use worker:item-id | evidence"
|
|
1038
|
+
)
|
|
1039
|
+
return worker, item_id, evidence.strip()
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def _group_from_fields(
|
|
1043
|
+
finding_id: str, fields: Mapping[str, list[str]]
|
|
1044
|
+
) -> dict[str, Any]:
|
|
1045
|
+
scalar = {
|
|
1046
|
+
key: values[0] if len(values) == 1 else ""
|
|
1047
|
+
for key, values in fields.items() if key != "source"
|
|
1048
|
+
}
|
|
1049
|
+
source_rows = fields.get("source", [])
|
|
1050
|
+
if source_rows:
|
|
1051
|
+
parsed = [_parse_group_source(spec) for spec in source_rows]
|
|
1052
|
+
else:
|
|
1053
|
+
specs = [part.strip() for part in scalar.get("sources", "").split(",") if part.strip()]
|
|
1054
|
+
if len(specs) != 1 or not scalar.get("evidence"):
|
|
1055
|
+
raise ConvergenceContractError(
|
|
1056
|
+
"multi-worker groups require one Source per worker"
|
|
1057
|
+
)
|
|
1058
|
+
worker, colon, item_id = specs[0].partition(":")
|
|
1059
|
+
if not colon or not worker or not item_id:
|
|
1060
|
+
raise ConvergenceContractError("Sources must use worker:item-id entries")
|
|
1061
|
+
parsed = [(worker, item_id, scalar["evidence"])]
|
|
1062
|
+
discovered, source_items = _group_source_provenance(parsed)
|
|
1063
|
+
origin_worker, separator, origin_item = scalar.get("origin", "").partition(":")
|
|
1064
|
+
if not separator or discovered.get(origin_worker, {}).get("itemId") != origin_item:
|
|
1065
|
+
raise ConvergenceContractError("Origin must identify one Source item")
|
|
1066
|
+
return {
|
|
1067
|
+
"findingId": finding_id, "summary": scalar.get("summary", ""),
|
|
1068
|
+
"category": scalar.get("category", ""),
|
|
1069
|
+
"ticketIds": [item.strip() for item in scalar.get("tickets", "").split(",") if item.strip()],
|
|
1070
|
+
"originWorker": origin_worker,
|
|
1071
|
+
"originEvidence": discovered[origin_worker]["evidence"],
|
|
1072
|
+
"discoveredBy": discovered,
|
|
1073
|
+
"sourceItems": source_items,
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _group_source_provenance(
|
|
1078
|
+
parsed: list[tuple[str, str, str]],
|
|
1079
|
+
) -> tuple[dict[str, dict[str, str]], list[dict[str, str]]]:
|
|
1080
|
+
workers = [worker for worker, _item, _evidence in parsed]
|
|
1081
|
+
if len(workers) != len(set(workers)):
|
|
1082
|
+
raise ConvergenceContractError("duplicate Source worker in one finding")
|
|
1083
|
+
discovered = {
|
|
1084
|
+
worker: {"itemId": item, "evidence": evidence}
|
|
1085
|
+
for worker, item, evidence in parsed
|
|
1086
|
+
}
|
|
1087
|
+
sources = [
|
|
1088
|
+
{"worker": worker, "itemId": item}
|
|
1089
|
+
for worker, item, _evidence in parsed
|
|
1090
|
+
]
|
|
1091
|
+
if any(discovered[row["worker"]]["itemId"] != row["itemId"] for row in sources):
|
|
1092
|
+
raise ConvergenceContractError("discoveredBy and sourceItems do not match")
|
|
1093
|
+
return discovered, sources
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _assignment_identity(row: Mapping[str, Any]) -> tuple[str, str, str]:
|
|
1097
|
+
role = str(row.get("role") or "")
|
|
1098
|
+
provider = str(row.get("provider") or "")
|
|
1099
|
+
model = str(row.get("modelExecutionValue") or row.get("model") or "")
|
|
1100
|
+
if not all((role, provider, model)):
|
|
1101
|
+
raise ConvergenceContractError(
|
|
1102
|
+
"worker assignment requires role, provider, and model"
|
|
1103
|
+
)
|
|
1104
|
+
return role, provider, model
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _execution_model(row: Mapping[str, Any]) -> str:
|
|
1108
|
+
binding = row.get("binding")
|
|
1109
|
+
if isinstance(binding, Mapping) and binding.get("resolvedExecutionValue"):
|
|
1110
|
+
return str(binding["resolvedExecutionValue"])
|
|
1111
|
+
return str(row.get("modelId") or "")
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
def _group_assignment_identity(
|
|
1115
|
+
manifest: Mapping[str, Any], worker_id: str,
|
|
1116
|
+
) -> tuple[str, str, str, int]:
|
|
1117
|
+
assignments = manifest.get("workerAssignments") if isinstance(manifest.get("workerAssignments"), list) else []
|
|
1118
|
+
normalized = worker_id.removesuffix("-worker")
|
|
1119
|
+
if normalized == "lead":
|
|
1120
|
+
duplicate = any(
|
|
1121
|
+
isinstance(row, Mapping)
|
|
1122
|
+
and str(row.get("workerId", "")).removesuffix("-worker") == "lead"
|
|
1123
|
+
for row in assignments
|
|
1124
|
+
)
|
|
1125
|
+
if duplicate:
|
|
1126
|
+
raise ConvergenceContractError("ambiguous lead assignment identity")
|
|
1127
|
+
lead = manifest.get("leadAssignment")
|
|
1128
|
+
if not isinstance(lead, Mapping):
|
|
1129
|
+
raise ConvergenceContractError("lead assignment identity is missing")
|
|
1130
|
+
role, provider, model = _assignment_identity(lead)
|
|
1131
|
+
if role != "lead":
|
|
1132
|
+
raise ConvergenceContractError("leadAssignment role must be lead")
|
|
1133
|
+
return "leader", provider, model, 1
|
|
1134
|
+
matches = [(index, row) for index, row in enumerate(assignments)
|
|
1135
|
+
if isinstance(row, Mapping)
|
|
1136
|
+
and str(row.get("workerId", "")).removesuffix("-worker") == normalized]
|
|
1137
|
+
if len(matches) != 1:
|
|
1138
|
+
raise ConvergenceContractError(
|
|
1139
|
+
f"worker assignment identity is missing or duplicate for {worker_id}"
|
|
1140
|
+
)
|
|
1141
|
+
index, assignment = matches[0]
|
|
1142
|
+
role, provider, model = _assignment_identity(assignment)
|
|
1143
|
+
ordinal = sum(
|
|
1144
|
+
1 for row in assignments[:index + 1]
|
|
1145
|
+
if isinstance(row, Mapping) and str(row.get("role") or "") == role
|
|
1146
|
+
)
|
|
1147
|
+
return role, provider, model, ordinal
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _group_worker(manifest: Mapping[str, Any], worker_id: str) -> dict[str, str]:
|
|
1151
|
+
role, provider, model, ordinal = _group_assignment_identity(
|
|
1152
|
+
manifest, worker_id
|
|
1153
|
+
)
|
|
1154
|
+
executions = manifest.get("roleExecutions") if isinstance(manifest.get("roleExecutions"), list) else []
|
|
1155
|
+
candidates = [row for row in executions if isinstance(row, Mapping)
|
|
1156
|
+
and row.get("role") == role and row.get("provider") == provider
|
|
1157
|
+
and _execution_model(row) == model and row.get("ordinal") == ordinal]
|
|
1158
|
+
if len(candidates) != 1:
|
|
1159
|
+
raise ConvergenceContractError(f"no canonical role execution for {worker_id}")
|
|
1160
|
+
execution = candidates[0]
|
|
1161
|
+
if not execution.get("participantRef") or not execution.get("roleExecutionRef"):
|
|
1162
|
+
raise ConvergenceContractError(f"no canonical role execution for {worker_id}")
|
|
1163
|
+
audience = "report-writer" if role == "report-writer" else "lead" if role in {"lead", "leader"} else "analysis"
|
|
1164
|
+
return {"workerId": worker_id, "audience": audience,
|
|
1165
|
+
"participantRef": str(execution["participantRef"]),
|
|
1166
|
+
"sourceRoleExecutionRef": str(execution["roleExecutionRef"])}
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def _convergence_config(authority: RunArtifactAuthority) -> Mapping[str, Any]:
|
|
1170
|
+
"""convergence 설정의 정본은 task-manifest.json 이다.
|
|
1171
|
+
|
|
1172
|
+
`render.render_task_manifest` 가 `_build_convergence_block` 결과를 거기에만
|
|
1173
|
+
쓴다. run manifest 에는 `convergenceStatePath` 뿐이라, 예전처럼 run manifest 를
|
|
1174
|
+
읽으면 키가 없어 `adversarial=False` / `verificationMode="lightweight"` 기본값으로
|
|
1175
|
+
조용히 떨어졌다. 그 강등은 무증상이다 — `convergence_engine._parse_groups` 가
|
|
1176
|
+
적대 모드가 꺼진 다중 출처 그룹을 큐에 넣지 않고 즉시 `full-consensus` 로
|
|
1177
|
+
확정하므로, 세 워커의 처방이 서로 달라도 합의로 기록된다.
|
|
1178
|
+
|
|
1179
|
+
설정이 없으면 기본값으로 떨어지지 않고 실패한다. 여기서 조용히 넘어가면
|
|
1180
|
+
검증 자체가 사라지고 산출물은 정상으로 읽힌다.
|
|
1181
|
+
"""
|
|
1182
|
+
path = task_manifest_path(authority.project_root, authority.payload)
|
|
1183
|
+
payload = load_owned_json_object(path)
|
|
1184
|
+
config = payload.get("convergence")
|
|
1185
|
+
if not isinstance(config, Mapping):
|
|
1186
|
+
raise ConvergenceContractError(
|
|
1187
|
+
f"task manifest carries no convergence config: {path}"
|
|
1188
|
+
)
|
|
1189
|
+
return config
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _prepare_groups(args: argparse.Namespace) -> tuple[str, Path]:
|
|
1193
|
+
authority = validated_run_authority(args.run_manifest)
|
|
1194
|
+
manifest = authority.payload
|
|
1195
|
+
groups = _parse_grouping_markdown(args.input)
|
|
1196
|
+
worker_ids = list(dict.fromkeys(
|
|
1197
|
+
source["worker"] for group in groups for source in group["sourceItems"]
|
|
1198
|
+
))
|
|
1199
|
+
output = authority.run_dir / "state" / _canonical_run_artifact_name(
|
|
1200
|
+
"convergence-groups", authority.task_type, authority.state_sequence
|
|
1201
|
+
)
|
|
1202
|
+
config = _convergence_config(authority)
|
|
1203
|
+
workers = [_group_worker(manifest, worker) for worker in worker_ids]
|
|
1204
|
+
source_refs = [worker["sourceRoleExecutionRef"] for worker in workers]
|
|
1205
|
+
if len(source_refs) != len(set(source_refs)):
|
|
1206
|
+
raise ConvergenceContractError("duplicate sourceRoleExecutionRef")
|
|
1207
|
+
payload = {"schemaVersion": "2.0", "executionIdentityVersion": 2,
|
|
1208
|
+
"taskKey": manifest.get("taskKey"), "runManifestPath": manifest.get("runManifestPath"),
|
|
1209
|
+
"config": {"enabled": bool(config.get("enabled", True)),
|
|
1210
|
+
"adversarial": bool(config.get("adversarial", False)),
|
|
1211
|
+
"maxRounds": int(config.get("maxRounds", 2)),
|
|
1212
|
+
"effectiveMaxRounds": int(config.get("effectiveMaxRounds", 2)),
|
|
1213
|
+
"verificationMode": str(config.get("verificationMode", "lightweight"))},
|
|
1214
|
+
"workers": workers, "groups": groups}
|
|
1215
|
+
schema_errors = validate_schema(
|
|
1216
|
+
payload, load_named_schema("convergence-groups-v2.0.schema.json")
|
|
1217
|
+
)
|
|
1218
|
+
if schema_errors:
|
|
1219
|
+
raise ConvergenceContractError(
|
|
1220
|
+
"invalid convergence groups: " + "; ".join(schema_errors)
|
|
1221
|
+
)
|
|
1222
|
+
seed_working_state(
|
|
1223
|
+
payload, execution_manifest=authority.execution_manifest
|
|
1224
|
+
)
|
|
1225
|
+
write_json_atomic(output, payload)
|
|
1226
|
+
return "prepared", output
|
|
1227
|
+
|
|
1228
|
+
|
|
912
1229
|
def _execute(args: argparse.Namespace) -> tuple[str, Path]:
|
|
913
1230
|
operations: dict[str, Any] = {
|
|
1231
|
+
"prepare-groups": _prepare_groups,
|
|
914
1232
|
"seed": _seed,
|
|
915
1233
|
"plan-round": _plan_round,
|
|
916
1234
|
"collect-results": _collect_results,
|
|
@@ -943,7 +1261,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
943
1261
|
except OSError as exc:
|
|
944
1262
|
print(f"error: {exc}", file=sys.stderr)
|
|
945
1263
|
return 1
|
|
946
|
-
|
|
1264
|
+
if args.operation == "prepare-groups":
|
|
1265
|
+
print("Convergence groups\n" + line("Status", "ready") + line("Action", action), end="")
|
|
1266
|
+
else:
|
|
1267
|
+
_emit(args.operation, action, path)
|
|
947
1268
|
return 0
|
|
948
1269
|
|
|
949
1270
|
|
|
@@ -21,6 +21,7 @@ _AUDIENCES = {"analysis", "lead", "report-writer"}
|
|
|
21
21
|
_FINDING_SOURCE_AUDIENCES = {"analysis", "lead"}
|
|
22
22
|
_AUDIENCE_SOURCE_ROLES = {
|
|
23
23
|
"analysis": frozenset({"analyser", "designer", "planner", "verifier"}),
|
|
24
|
+
"lead": frozenset({"leader"}),
|
|
24
25
|
"report-writer": frozenset({"report-writer"}),
|
|
25
26
|
}
|
|
26
27
|
_VERIFICATION_MODES = {"lightweight", "full-reanalysis"}
|
|
@@ -809,7 +810,7 @@ def _parse_critic_dispatches(
|
|
|
809
810
|
worker = _required_string(row, "worker", f"critic dispatches[{index}]")
|
|
810
811
|
if worker in seen:
|
|
811
812
|
raise ConvergenceContractError(f"duplicate critic voter dispatch: {worker}")
|
|
812
|
-
if worker not in roster
|
|
813
|
+
if worker not in roster:
|
|
813
814
|
raise ConvergenceContractError(
|
|
814
815
|
f"critic voter must be a non-critic analyser: {worker}"
|
|
815
816
|
)
|
|
@@ -823,9 +824,7 @@ def _parse_critic_dispatches(
|
|
|
823
824
|
)
|
|
824
825
|
seen.add(worker)
|
|
825
826
|
dispatches.append({"worker": worker, "status": status, "durationMs": duration})
|
|
826
|
-
expected =
|
|
827
|
-
worker for worker in roster if not _is_critic_worker(worker, provider)
|
|
828
|
-
}
|
|
827
|
+
expected = set(roster)
|
|
829
828
|
if seen != expected:
|
|
830
829
|
missing = sorted(expected - seen)
|
|
831
830
|
raise ConvergenceContractError(
|
|
@@ -869,7 +868,7 @@ def _parse_critic_gap(
|
|
|
869
868
|
votes_value = _object(gap.get("votes"), f"{label}.votes")
|
|
870
869
|
votes: dict[str, dict[str, Any]] = {}
|
|
871
870
|
for worker, raw_vote in votes_value.items():
|
|
872
|
-
if worker not in roster
|
|
871
|
+
if worker not in roster:
|
|
873
872
|
raise ConvergenceContractError(
|
|
874
873
|
f"critic voter must be a non-critic analyser: {worker}"
|
|
875
874
|
)
|
|
@@ -1435,15 +1434,9 @@ def _validate_critic_roster(
|
|
|
1435
1434
|
errors.append("criticVerification.analyserRoster does not match workers")
|
|
1436
1435
|
for row in dispatches if isinstance(dispatches, list) else []:
|
|
1437
1436
|
worker = row.get("worker") if isinstance(row, Mapping) else None
|
|
1438
|
-
if (
|
|
1439
|
-
not _nonempty_string(worker)
|
|
1440
|
-
or worker not in roster
|
|
1441
|
-
or _is_critic_worker(worker, provider)
|
|
1442
|
-
):
|
|
1437
|
+
if not _nonempty_string(worker) or worker not in roster:
|
|
1443
1438
|
errors.append(f"critic voter must be a non-critic analyser: {worker}")
|
|
1444
|
-
expected_dispatches =
|
|
1445
|
-
worker for worker in roster if not _is_critic_worker(worker, provider)
|
|
1446
|
-
}
|
|
1439
|
+
expected_dispatches = set(roster)
|
|
1447
1440
|
actual_dispatches = {
|
|
1448
1441
|
row.get("worker")
|
|
1449
1442
|
for row in dispatches
|
|
@@ -1467,17 +1460,13 @@ def _validate_critic_roster(
|
|
|
1467
1460
|
for gap in gaps if isinstance(gaps, list) else []:
|
|
1468
1461
|
votes = gap.get("votes") if isinstance(gap, Mapping) else None
|
|
1469
1462
|
for voter in votes if isinstance(votes, Mapping) else {}:
|
|
1470
|
-
if voter not in roster
|
|
1463
|
+
if voter not in roster:
|
|
1471
1464
|
errors.append(f"critic voter must be a non-critic analyser: {voter}")
|
|
1472
1465
|
elif voter not in completed:
|
|
1473
1466
|
errors.append(f"critic voter has no completed dispatch: {voter}")
|
|
1474
1467
|
return errors
|
|
1475
1468
|
|
|
1476
1469
|
|
|
1477
|
-
def _is_critic_worker(worker: Any, provider: Any) -> bool:
|
|
1478
|
-
return isinstance(provider, str) and worker in {provider, f"{provider}-worker"}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
1470
|
def _final_collaborative_classification(finding: Mapping[str, Any]) -> str:
|
|
1482
1471
|
usable: list[Mapping[str, Any]] = []
|
|
1483
1472
|
for round_row in finding.get("rounds", []):
|
|
@@ -2135,6 +2124,9 @@ def _validate_worker_execution_identity(
|
|
|
2135
2124
|
f"{worker['audience']} audience source role is not allowed: "
|
|
2136
2125
|
f"{source.role}"
|
|
2137
2126
|
)
|
|
2127
|
+
source_refs = [worker["sourceRoleExecutionRef"] for worker in workers]
|
|
2128
|
+
if len(source_refs) != len(set(source_refs)):
|
|
2129
|
+
raise ConvergenceContractError("duplicate sourceRoleExecutionRef")
|
|
2138
2130
|
|
|
2139
2131
|
|
|
2140
2132
|
def _parse_groups(
|