okstra 0.207.0 → 0.208.0
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 +3 -2
- package/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.mjs +1 -1
- package/dist/commands/lifecycle/doctor.mjs +1 -1
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/docs/architecture/storage-model.md +14 -0
- package/docs/architecture.md +30 -9
- package/docs/cli.md +26 -22
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/project-structure-overview.md +15 -8
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/operations/explain-flow.json +6 -0
- package/runtime/bin/lib/okstra/cli.sh +1 -5
- package/runtime/bin/lib/okstra/globals.sh +0 -2
- package/runtime/bin/lib/okstra/usage.sh +5 -3
- package/runtime/bin/okstra.sh +0 -2
- package/runtime/prompts/duties/business-flow-investigator.json +14 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +22 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +12 -17
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +0 -91
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +6 -0
- package/runtime/python/okstra_ctl/agent/invocation.py +1 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +1 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +11 -125
- package/runtime/python/okstra_ctl/agent/standalone.py +183 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +39 -8
- package/runtime/python/okstra_ctl/assignment_resolver.py +7 -1
- package/runtime/python/okstra_ctl/brief_frontmatter.py +10 -0
- package/runtime/python/okstra_ctl/business_flow/__init__.py +4 -0
- package/runtime/python/okstra_ctl/business_flow/cli.py +134 -0
- package/runtime/python/okstra_ctl/business_flow/contracts.py +268 -0
- package/runtime/python/okstra_ctl/business_flow/engine.py +518 -0
- package/runtime/python/okstra_ctl/business_flow/hooks.py +221 -0
- package/runtime/python/okstra_ctl/business_flow/invocation.py +170 -0
- package/runtime/python/okstra_ctl/business_flow/report.py +49 -0
- package/runtime/python/okstra_ctl/business_flow/source.py +206 -0
- package/runtime/python/okstra_ctl/business_flow/store.py +388 -0
- package/runtime/python/okstra_ctl/convergence.py +173 -2
- package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +18 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +8 -0
- package/runtime/python/okstra_ctl/coverage_census.py +596 -0
- package/runtime/python/okstra_ctl/design_surfaces.py +4 -0
- package/runtime/python/okstra_ctl/direct_work.py +1 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +7 -3
- package/runtime/python/okstra_ctl/doctor.py +12 -6
- package/runtime/python/okstra_ctl/domain/role.py +1 -0
- package/runtime/python/okstra_ctl/group_context.py +5 -4
- package/runtime/python/okstra_ctl/legacy_model_selection.py +7 -51
- package/runtime/python/okstra_ctl/manager_split.py +4 -1
- package/runtime/python/okstra_ctl/manager_view.py +9 -3
- package/runtime/python/okstra_ctl/model_io/lines.py +1 -24
- package/runtime/python/okstra_ctl/model_io/renderers.py +54 -41
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/error_analysis/profile.md +6 -8
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/feature_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/final_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -8
- package/runtime/python/okstra_ctl/phases/implementation/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation/profile.md +0 -6
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation-input.template.md +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +4 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +1 -14
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +5 -8
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +15 -5
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +9 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +4 -2
- package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +25 -9
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +7 -10
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.md +4 -11
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/project_analysis/profile.md +0 -8
- package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +1 -1
- package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.md +10 -8
- package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +3 -3
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.json +1 -1
- package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +0 -4
- package/runtime/python/okstra_ctl/plan_items.py +1 -1
- package/runtime/python/okstra_ctl/render.py +10 -43
- package/runtime/python/okstra_ctl/render_final_report.py +3 -0
- package/runtime/python/okstra_ctl/report_assembly.py +15 -1
- package/runtime/python/okstra_ctl/report_finalize.py +40 -0
- package/runtime/python/okstra_ctl/report_html/render.py +3 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -2
- package/runtime/python/okstra_ctl/run.py +78 -409
- package/runtime/python/okstra_ctl/wizard/__init__.py +2 -24
- package/runtime/python/okstra_ctl/wizard/cli.py +3 -6
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -35
- package/runtime/python/okstra_ctl/wizard/engine.py +2 -4
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -88
- package/runtime/python/okstra_ctl/wizard/registry.py +36 -228
- package/runtime/python/okstra_ctl/wizard/render.py +2 -2
- package/runtime/python/okstra_ctl/wizard/roles.py +1 -3
- package/runtime/python/okstra_ctl/wizard/sources.py +9 -40
- package/runtime/python/okstra_ctl/wizard/state.py +36 -145
- package/runtime/python/okstra_ctl/wizard/statefile.py +27 -128
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +22 -10
- package/runtime/python/okstra_ctl/wizard/steps_options.py +5 -4
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +15 -565
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +9 -2
- package/runtime/schemas/business-flow-v1.schema.json +847 -0
- package/runtime/schemas/convergence-groups-v2.0.schema.json +7 -0
- package/runtime/skills/okstra-explain-flow/SKILL.md +42 -0
- package/runtime/skills/okstra-inspect/facets/history.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/templates/manager/view.template.html +7 -4
- package/runtime/templates/reports/business-flow.template.md +106 -0
- package/runtime/templates/reports/html/base.template.html +14 -1
- package/runtime/templates/reports/html/business-flow.template.html +31 -0
- package/runtime/templates/reports/html/i18n/en.json +1 -0
- package/runtime/templates/reports/html/i18n/ko.json +1 -0
- package/runtime/templates/worker-prompt-preamble.md +11 -2
- package/runtime/validators/checks/validate-prompt-metadata-01.py +10 -10
- package/runtime/validators/validate-run.py +70 -21
- package/runtime/validators/validate_analysis_report.py +21 -21
- package/runtime/python/okstra_ctl/workers.py +0 -133
|
@@ -174,18 +174,18 @@ def _validate_all_current_code_evidence(
|
|
|
174
174
|
|
|
175
175
|
|
|
176
176
|
|
|
177
|
-
def
|
|
177
|
+
def _selected_worker_roles(
|
|
178
178
|
run_manifest: dict, errors: list[str]
|
|
179
179
|
) -> tuple[str, ...]:
|
|
180
180
|
contract = run_manifest.get("teamContract")
|
|
181
181
|
entries = (
|
|
182
|
-
contract.get("
|
|
182
|
+
contract.get("workerRoles")
|
|
183
183
|
if isinstance(contract, dict)
|
|
184
184
|
else None
|
|
185
185
|
)
|
|
186
186
|
if not isinstance(entries, list) or not entries:
|
|
187
187
|
errors.append(
|
|
188
|
-
"run-manifest.teamContract.
|
|
188
|
+
"run-manifest.teamContract.workerRoles must be a non-empty "
|
|
189
189
|
"array of worker contract objects"
|
|
190
190
|
)
|
|
191
191
|
return ()
|
|
@@ -198,13 +198,13 @@ def _required_worker_roles(
|
|
|
198
198
|
or role != role.strip()
|
|
199
199
|
):
|
|
200
200
|
errors.append(
|
|
201
|
-
"run-manifest.teamContract.
|
|
201
|
+
"run-manifest.teamContract.workerRoles"
|
|
202
202
|
f"[{index}].role must be an exact non-empty string"
|
|
203
203
|
)
|
|
204
204
|
return ()
|
|
205
205
|
if role in roles:
|
|
206
206
|
errors.append(
|
|
207
|
-
"run-manifest.teamContract.
|
|
207
|
+
"run-manifest.teamContract.workerRoles must not contain "
|
|
208
208
|
f"duplicate role `{role}`"
|
|
209
209
|
)
|
|
210
210
|
return ()
|
|
@@ -212,17 +212,17 @@ def _required_worker_roles(
|
|
|
212
212
|
return tuple(roles)
|
|
213
213
|
|
|
214
214
|
|
|
215
|
-
def
|
|
216
|
-
|
|
215
|
+
def _selected_worker_status_rows(data: dict, selected_roles: tuple[str, ...]) -> list[dict]:
|
|
216
|
+
selected = set(selected_roles)
|
|
217
217
|
return [
|
|
218
218
|
row
|
|
219
219
|
for row in data.get("executionStatus") or []
|
|
220
|
-
if isinstance(row, dict) and row.get("role") in
|
|
220
|
+
if isinstance(row, dict) and row.get("role") in selected
|
|
221
221
|
]
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
def _validate_execution_status_roles(
|
|
225
|
-
data: dict,
|
|
225
|
+
data: dict, selected_roles: tuple[str, ...], errors: list[str]
|
|
226
226
|
) -> bool:
|
|
227
227
|
"""Validate exact labels and report whether verdict matching is unambiguous."""
|
|
228
228
|
actual_roles = {
|
|
@@ -230,7 +230,7 @@ def _validate_execution_status_roles(
|
|
|
230
230
|
for row in data.get("executionStatus") or []
|
|
231
231
|
if isinstance(row, dict)
|
|
232
232
|
}
|
|
233
|
-
expected_by_fold = {role.casefold(): role for role in
|
|
233
|
+
expected_by_fold = {role.casefold(): role for role in selected_roles}
|
|
234
234
|
verdict_roles_unambiguous = True
|
|
235
235
|
for actual_role in sorted(actual_roles):
|
|
236
236
|
expected_role = expected_by_fold.get(actual_role.casefold())
|
|
@@ -241,10 +241,10 @@ def _validate_execution_status_roles(
|
|
|
241
241
|
f"expected exact role `{expected_role}`"
|
|
242
242
|
)
|
|
243
243
|
verdict_roles_unambiguous = False
|
|
244
|
-
for
|
|
245
|
-
if
|
|
244
|
+
for selected_role in selected_roles:
|
|
245
|
+
if selected_role in actual_roles:
|
|
246
246
|
continue
|
|
247
|
-
errors.append(f"missing
|
|
247
|
+
errors.append(f"missing selected analysis role `{selected_role}`")
|
|
248
248
|
return verdict_roles_unambiguous
|
|
249
249
|
|
|
250
250
|
|
|
@@ -291,13 +291,13 @@ def _scope_confirmation_status(
|
|
|
291
291
|
|
|
292
292
|
def _validate_analysis_verdict(
|
|
293
293
|
data: dict,
|
|
294
|
-
|
|
294
|
+
selected_roles: tuple[str, ...],
|
|
295
295
|
reporter_confirmation: str | None,
|
|
296
296
|
errors: list[str],
|
|
297
297
|
) -> None:
|
|
298
298
|
common = data.get("analysisCommon") or {}
|
|
299
299
|
scope = common.get("scope") or {}
|
|
300
|
-
worker_rows =
|
|
300
|
+
worker_rows = _selected_worker_status_rows(data, selected_roles)
|
|
301
301
|
worker_blocked = not any(row.get("status") == "completed" for row in worker_rows)
|
|
302
302
|
scope_blocked = reporter_confirmation != "complete"
|
|
303
303
|
unresolved_review = any(
|
|
@@ -316,7 +316,7 @@ def _validate_analysis_verdict(
|
|
|
316
316
|
return
|
|
317
317
|
reasons = []
|
|
318
318
|
if worker_blocked:
|
|
319
|
-
reasons.append("
|
|
319
|
+
reasons.append("selected analysis workers produced zero completed results")
|
|
320
320
|
if scope_blocked:
|
|
321
321
|
reasons.append("brief frontmatter reporter-confirmations is not complete")
|
|
322
322
|
if partial:
|
|
@@ -330,7 +330,7 @@ def _validate_analysis_verdict(
|
|
|
330
330
|
|
|
331
331
|
def _validate_scope_before_dispatch(
|
|
332
332
|
data: dict,
|
|
333
|
-
|
|
333
|
+
selected_roles: tuple[str, ...],
|
|
334
334
|
reporter_confirmation: str | None,
|
|
335
335
|
errors: list[str],
|
|
336
336
|
) -> None:
|
|
@@ -338,11 +338,11 @@ def _validate_scope_before_dispatch(
|
|
|
338
338
|
return
|
|
339
339
|
dispatched = any(
|
|
340
340
|
row.get("status") != "not-run"
|
|
341
|
-
for row in
|
|
341
|
+
for row in _selected_worker_status_rows(data, selected_roles)
|
|
342
342
|
)
|
|
343
343
|
if dispatched:
|
|
344
344
|
errors.append(
|
|
345
|
-
"
|
|
345
|
+
"analysis workers were dispatched before "
|
|
346
346
|
"reporter-confirmations: complete was recorded"
|
|
347
347
|
)
|
|
348
348
|
|
|
@@ -354,9 +354,9 @@ def validate_analysis_semantics(
|
|
|
354
354
|
reporter_confirmation: str | None,
|
|
355
355
|
errors: list[str],
|
|
356
356
|
) -> None:
|
|
357
|
-
|
|
357
|
+
selected_roles = _selected_worker_roles(run_manifest, errors)
|
|
358
358
|
analysis_worker_roles = tuple(
|
|
359
|
-
role for role in
|
|
359
|
+
role for role in selected_roles if not _is_report_writer_role(role)
|
|
360
360
|
)
|
|
361
361
|
verdict_roles_unambiguous = _validate_execution_status_roles(
|
|
362
362
|
data, analysis_worker_roles, errors
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
"""V1 provider-worker roster compatibility parsing.
|
|
2
|
-
|
|
3
|
-
Profile 파일에서 권장 worker 목록을 뽑고(`resolve_profile_workers`), 사용자
|
|
4
|
-
오버라이드와 합쳐 정규화한다(`normalize_workers`). bash workers.sh 의
|
|
5
|
-
동등한 python 구현.
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from .models import provider_ids
|
|
12
|
-
|
|
13
|
-
ALLOWED_WORKERS = provider_ids("analyser") + ["report-writer"]
|
|
14
|
-
DEFAULT_WORKERS = ["claude", "codex", "report-writer"]
|
|
15
|
-
PROFILE_BULLET_HEADERS = {
|
|
16
|
-
"- Workers:",
|
|
17
|
-
"- Required workers:",
|
|
18
|
-
"- Reviewers:",
|
|
19
|
-
"- Analysers:",
|
|
20
|
-
}
|
|
21
|
-
PROFILE_BULLET_HEADERS_OPTIONAL = {
|
|
22
|
-
"- Optional workers:",
|
|
23
|
-
"- Optional workers (opt-in via `--workers`):",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class WorkersError(Exception):
|
|
28
|
-
"""invalid worker selection — surface to user."""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _resolve_v1_workers_under(profile_path: Path, headers: set[str]) -> list[str]:
|
|
32
|
-
"""Read legacy provider-worker bullets without interpreting role YAML.
|
|
33
|
-
|
|
34
|
-
Collect ` - <id> …` sub-bullets under any of `headers`
|
|
35
|
-
until the next top-level bullet. Returns first token (before any
|
|
36
|
-
` — ` / ` -- ` / whitespace) of each captured line.
|
|
37
|
-
"""
|
|
38
|
-
if not Path(profile_path).is_file():
|
|
39
|
-
return []
|
|
40
|
-
capturing = False
|
|
41
|
-
out: list[str] = []
|
|
42
|
-
for line in Path(profile_path).read_text(encoding="utf-8").splitlines():
|
|
43
|
-
stripped = line.strip()
|
|
44
|
-
if stripped in headers:
|
|
45
|
-
capturing = True
|
|
46
|
-
continue
|
|
47
|
-
if not capturing:
|
|
48
|
-
continue
|
|
49
|
-
if line.startswith("- "):
|
|
50
|
-
break
|
|
51
|
-
if line.startswith(" - "):
|
|
52
|
-
body = line[4:].strip()
|
|
53
|
-
token = body.split(" — ", 1)[0].split(" -- ", 1)[0].split()[0]
|
|
54
|
-
out.append(token)
|
|
55
|
-
continue
|
|
56
|
-
if stripped:
|
|
57
|
-
break
|
|
58
|
-
return out
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def resolve_profile_workers(profile_path: Path) -> list[str]:
|
|
62
|
-
"""`prompts/profiles/<task-type>.md` 본문의 `- Required workers:` (또는
|
|
63
|
-
`- Workers:` / `- Reviewers:` / `- Analysers:`) 섹션 아래 sub-bullet 들을
|
|
64
|
-
worker id 리스트로 돌려준다. profile 파일이 없거나 섹션이 없으면 빈 리스트.
|
|
65
|
-
"""
|
|
66
|
-
return _resolve_v1_workers_under(profile_path, PROFILE_BULLET_HEADERS)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def resolve_optional_workers(profile_path: Path) -> list[str]:
|
|
70
|
-
r"""`- Optional workers (opt-in via \`--workers\`):` 섹션 아래 sub-bullet
|
|
71
|
-
들에서 worker id 만 추출한다. (` - antigravity — when added …` → `antigravity`)
|
|
72
|
-
Required 와 중복되는 항목은 제거. ALLOWED_WORKERS 밖 토큰도 제거.
|
|
73
|
-
"""
|
|
74
|
-
raw = _resolve_v1_workers_under(profile_path, PROFILE_BULLET_HEADERS_OPTIONAL)
|
|
75
|
-
required = set(resolve_profile_workers(profile_path))
|
|
76
|
-
seen: set[str] = set()
|
|
77
|
-
out: list[str] = []
|
|
78
|
-
for w in raw:
|
|
79
|
-
if w in required or w in seen or w not in ALLOWED_WORKERS:
|
|
80
|
-
continue
|
|
81
|
-
seen.add(w)
|
|
82
|
-
out.append(w)
|
|
83
|
-
return out
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def normalize_workers(value: str) -> list[str]:
|
|
87
|
-
"""CSV 입력을 정규화한다.
|
|
88
|
-
|
|
89
|
-
- 공백 strip, 소문자화, 중복 제거(첫 출현 우선).
|
|
90
|
-
- 빈 입력이면 `DEFAULT_WORKERS` 를 default 로 사용 (antigravity 제외).
|
|
91
|
-
- 허용 외 worker 가 포함되면 `WorkersError`.
|
|
92
|
-
"""
|
|
93
|
-
items = [v.strip().lower() for v in (value or "").split(",") if v.strip()]
|
|
94
|
-
source = items or DEFAULT_WORKERS
|
|
95
|
-
unknown = [v for v in source if v not in ALLOWED_WORKERS]
|
|
96
|
-
if unknown:
|
|
97
|
-
raise WorkersError(f"unknown workers: {','.join(unknown)}")
|
|
98
|
-
seen: set[str] = set()
|
|
99
|
-
out: list[str] = []
|
|
100
|
-
for v in source:
|
|
101
|
-
if v in seen:
|
|
102
|
-
continue
|
|
103
|
-
seen.add(v)
|
|
104
|
-
out.append(v)
|
|
105
|
-
return out
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def validate_workers_against_profile(
|
|
109
|
-
workers: list[str],
|
|
110
|
-
profile_workers: list[str],
|
|
111
|
-
optional_workers: list[str] | None = None,
|
|
112
|
-
) -> None:
|
|
113
|
-
"""프로파일이 `Required workers:` 로 로스터를 선언했다면, 사용자
|
|
114
|
-
override 가 (required ∪ optional) 의 부분집합인지 검증한다.
|
|
115
|
-
|
|
116
|
-
`Required workers:` 는 이 시스템에서 **allowlist + 기본 로스터**다 —
|
|
117
|
-
전원 필수 집합이 아니다. 사용자는 그 안에서 좁혀 고를 수 있고(최소 1),
|
|
118
|
-
wizard 는 분석 워커만 고르게 한 뒤 report-writer 를 뒤에서 강제
|
|
119
|
-
추가하므로 누락 검증을 여기서 하면 그 흐름이 깨진다.
|
|
120
|
-
|
|
121
|
-
`profile_workers` 가 비어 있으면(프로파일이 로스터를 선언하지 않은
|
|
122
|
-
구버전) 검증을 건너뛴다.
|
|
123
|
-
"""
|
|
124
|
-
if not profile_workers:
|
|
125
|
-
return
|
|
126
|
-
allowed = set(profile_workers) | set(optional_workers or [])
|
|
127
|
-
extras = [w for w in workers if w not in allowed]
|
|
128
|
-
if extras:
|
|
129
|
-
allow_str = ",".join(profile_workers + list(optional_workers or []))
|
|
130
|
-
raise WorkersError(
|
|
131
|
-
"workers not allowed by profile roster: "
|
|
132
|
-
f"{','.join(extras)} (profile allows: {allow_str})"
|
|
133
|
-
)
|