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,39 @@
|
|
|
1
|
+
"""모델용 고정 텍스트 투영의 scalar 줄 형식을 공유한다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def scalar(value: object) -> str:
|
|
8
|
+
"""복합 값과 Markdown 줄 삽입을 노출하지 않는 scalar 문자열이다."""
|
|
9
|
+
if value is None or value == "" or not isinstance(value, (str, int, float, bool)):
|
|
10
|
+
return "-"
|
|
11
|
+
text = str(value).replace("`", "\\`").replace("\n", " ").replace("\r", " ")
|
|
12
|
+
return re.sub(
|
|
13
|
+
r"[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\x9f\u2028\u2029]",
|
|
14
|
+
lambda match: f"\\u{ord(match.group(0)):04X}",
|
|
15
|
+
text,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def line(label: str, value: object) -> str:
|
|
20
|
+
"""한 scalar를 Markdown 목록 한 줄로 렌더한다."""
|
|
21
|
+
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9 ()/.-]*", label):
|
|
22
|
+
raise ValueError("fixed text label must be a static single-line label")
|
|
23
|
+
return f"- {label}: `{scalar(value)}`\n"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def value_lines(label: str, value: object) -> list[str]:
|
|
27
|
+
"""JSON 모양의 복합 값을 이름/순서/값 고정 줄로 손실 없이 펼친다."""
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
rows = [line(f"{label} field count", len(value))]
|
|
30
|
+
for index, (name, child) in enumerate(value.items(), 1):
|
|
31
|
+
rows.append(line(f"{label} field {index} name", name))
|
|
32
|
+
rows.extend(value_lines(f"{label} field {index} value", child))
|
|
33
|
+
return rows
|
|
34
|
+
if isinstance(value, list):
|
|
35
|
+
rows = [line(f"{label} item count", len(value))]
|
|
36
|
+
for index, child in enumerate(value, 1):
|
|
37
|
+
rows.extend(value_lines(f"{label} item {index}", child))
|
|
38
|
+
return rows
|
|
39
|
+
return [line(label, value)]
|
|
@@ -14,6 +14,7 @@ from dataclasses import dataclass
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Dict, List, Optional, Tuple
|
|
16
16
|
|
|
17
|
+
from .fixed_text import line
|
|
17
18
|
from .worktree import _resolve_commit_sha
|
|
18
19
|
|
|
19
20
|
|
|
@@ -202,7 +203,7 @@ def guidance(*, plan_run_root: Path, project_id: str, task_group: str,
|
|
|
202
203
|
f"--task-id {task_id} --work-category {work_category}")
|
|
203
204
|
return ("Recorded stage SHAs no longer match the git history "
|
|
204
205
|
"(external rebase/squash/amend?). Inspect and reconcile:\n"
|
|
205
|
-
f" {base} --check --
|
|
206
|
+
f" {base} --check --text\n"
|
|
206
207
|
f" {base} --apply --stage <N> --use-ref <branch|sha>")
|
|
207
208
|
|
|
208
209
|
|
|
@@ -272,9 +273,8 @@ def _items_as_json(items: List[StaleItem]) -> list:
|
|
|
272
273
|
"suggested_commit": i.suggested_commit} for i in items]
|
|
273
274
|
|
|
274
275
|
|
|
275
|
-
def
|
|
276
|
+
def _parse_args(argv: Optional[list]):
|
|
276
277
|
import argparse
|
|
277
|
-
import json as _json
|
|
278
278
|
|
|
279
279
|
p = argparse.ArgumentParser(
|
|
280
280
|
prog="okstra git-reconcile",
|
|
@@ -294,11 +294,44 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
294
294
|
p.add_argument("--use-ref", default="")
|
|
295
295
|
p.add_argument("--reset-anchor", default="")
|
|
296
296
|
p.add_argument("--json", action="store_true")
|
|
297
|
+
p.add_argument("--text", action="store_true")
|
|
297
298
|
a = p.parse_args(argv)
|
|
298
299
|
if a.check and a.apply:
|
|
299
300
|
p.error("--check and --apply are mutually exclusive")
|
|
300
301
|
if a.apply and a.stage is not None and not a.use_ref:
|
|
301
302
|
p.error("--stage requires --use-ref (confirm 항목은 명시 ref 로만 보정된다)")
|
|
303
|
+
return a
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _emit_result(payload: dict, *, as_json: bool, as_text: bool) -> None:
|
|
307
|
+
import json as _json
|
|
308
|
+
|
|
309
|
+
print(
|
|
310
|
+
render_git_reconcile_text(payload)
|
|
311
|
+
if as_text
|
|
312
|
+
else _json.dumps(payload, ensure_ascii=False, indent=None if as_json else 2)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def render_git_reconcile_text(payload: dict) -> str:
|
|
317
|
+
"""reconcile 결과의 승인된 분류만 고정 순서로 투영한다."""
|
|
318
|
+
rows = ["Okstra git reconcile\n"]
|
|
319
|
+
rows.append(line("Status", "ready" if payload.get("ok", True) else "error"))
|
|
320
|
+
items = payload.get("items") if isinstance(payload.get("items"), list) else []
|
|
321
|
+
rows.append(line("Item count", len(items)))
|
|
322
|
+
for index, item in enumerate(items, 1):
|
|
323
|
+
if not isinstance(item, dict):
|
|
324
|
+
continue
|
|
325
|
+
rows.append(line(f"Item {index} kind", item.get("kind")))
|
|
326
|
+
rows.append(line(f"Item {index} classification", item.get("classification")))
|
|
327
|
+
rows.append(line(f"Item {index} reason", item.get("reason")))
|
|
328
|
+
if "error" in payload:
|
|
329
|
+
rows.append(line("Failure reason", payload.get("error")))
|
|
330
|
+
return "".join(rows)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def main(argv: Optional[list] = None) -> int:
|
|
334
|
+
a = _parse_args(argv)
|
|
302
335
|
|
|
303
336
|
kw = dict(project_root=Path(a.project_root).resolve(),
|
|
304
337
|
plan_run_root=Path(a.plan_run_root).resolve(),
|
|
@@ -310,17 +343,16 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
310
343
|
reset_anchor=a.reset_anchor)
|
|
311
344
|
out = {"applied": result["applied"],
|
|
312
345
|
"remaining_confirm": _items_as_json(result["remaining_confirm"])}
|
|
313
|
-
|
|
314
|
-
indent=None if a.json else 2))
|
|
346
|
+
_emit_result(out, as_json=a.json, as_text=a.text)
|
|
315
347
|
return 2 if result["remaining_confirm"] else 0
|
|
316
348
|
items = classify_task(**kw)
|
|
317
349
|
stale = [i for i in items if i.classification != "ok"]
|
|
318
|
-
|
|
319
|
-
|
|
350
|
+
out = {"items": _items_as_json(stale)}
|
|
351
|
+
_emit_result(out, as_json=a.json, as_text=a.text)
|
|
320
352
|
return 2 if any(i.classification == "confirm" for i in stale) else 0
|
|
321
353
|
except ReconcileError as exc:
|
|
322
|
-
|
|
323
|
-
|
|
354
|
+
out = {"ok": False, "error": str(exc)}
|
|
355
|
+
_emit_result(out, as_json=a.json, as_text=a.text)
|
|
324
356
|
return 1
|
|
325
357
|
|
|
326
358
|
|
|
@@ -15,6 +15,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
|
15
15
|
from . import consumers, stage_targets, worktree_registry
|
|
16
16
|
from .final_report_paths import final_report_markdown_path
|
|
17
17
|
from .paths import RunRef
|
|
18
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
18
19
|
from .release_gate import (
|
|
19
20
|
blocking_condition_ids,
|
|
20
21
|
release_handoff_allowed,
|
|
@@ -75,8 +76,8 @@ def latest_whole_task_fv_release_ready(project_root, project_id: str,
|
|
|
75
76
|
for dj in sorted(reports_dir.glob("final-report-*.data.json"),
|
|
76
77
|
reverse=True):
|
|
77
78
|
try:
|
|
78
|
-
data =
|
|
79
|
-
except
|
|
79
|
+
data = load_owned_object(dj, artifact="final verification report")
|
|
80
|
+
except JsonBoundaryError:
|
|
80
81
|
continue
|
|
81
82
|
if ((data.get("header") or {}).get("taskType") == "final-verification"
|
|
82
83
|
and data.get("verificationScope") == "whole-task"
|
|
@@ -372,8 +373,8 @@ def record_verified(*, plan_run_root, stage: int, report_path: str,
|
|
|
372
373
|
막는다. 통과 조건은 whole-task 와 같고(`release_gate`), 기록되는 verdict 는
|
|
373
374
|
보고서가 실제로 실은 토큰이다."""
|
|
374
375
|
try:
|
|
375
|
-
data =
|
|
376
|
-
except
|
|
376
|
+
data = load_owned_object(Path(data_json), artifact="final verification report")
|
|
377
|
+
except JsonBoundaryError as exc:
|
|
377
378
|
raise HandoffError(f"cannot read final-report data.json: {exc}")
|
|
378
379
|
if (data.get("header") or {}).get("taskType") != "final-verification":
|
|
379
380
|
raise HandoffError("data.json is not a final-verification report")
|
|
@@ -11,6 +11,8 @@ import os
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Callable
|
|
13
13
|
|
|
14
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
15
|
+
|
|
14
16
|
SUPPORTED_LANGS = ("en", "ko")
|
|
15
17
|
DICTIONARY_REL = ("templates", "reports", "i18n")
|
|
16
18
|
# The human HTML has its own fixed strings — table headings, empty states,
|
|
@@ -48,8 +50,8 @@ def load_dictionary(lang: str, rel: tuple[str, ...] = DICTIONARY_REL) -> dict[st
|
|
|
48
50
|
)
|
|
49
51
|
path = _i18n_dir(rel) / f"{lang}.json"
|
|
50
52
|
try:
|
|
51
|
-
return
|
|
52
|
-
except
|
|
53
|
+
return load_owned_object(path, artifact="i18n dictionary")
|
|
54
|
+
except JsonBoundaryError as exc:
|
|
53
55
|
raise I18nError(f"failed to load {path}: {exc}") from exc
|
|
54
56
|
|
|
55
57
|
|
|
@@ -20,6 +20,11 @@ from .final_report_schema import (
|
|
|
20
20
|
load_schema_for_data,
|
|
21
21
|
validate as validate_schema,
|
|
22
22
|
)
|
|
23
|
+
from .json_boundary import (
|
|
24
|
+
JsonBoundaryError,
|
|
25
|
+
load_owned_object_snapshot,
|
|
26
|
+
write_owned_object_atomic,
|
|
27
|
+
)
|
|
23
28
|
from .report_views import normalize_direction_selection_identity
|
|
24
29
|
from .scope_provenance import brief_end_state_id_sequence
|
|
25
30
|
from .user_response import UserResponseError, parse_direction_selection
|
|
@@ -139,14 +144,14 @@ def _sibling_data_path(report: Path) -> Path:
|
|
|
139
144
|
return data_path
|
|
140
145
|
|
|
141
146
|
|
|
142
|
-
def _parse_data(data_path: Path
|
|
147
|
+
def _parse_data(data_path: Path) -> tuple[dict[str, Any], bytes]:
|
|
143
148
|
try:
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
snapshot = load_owned_object_snapshot(
|
|
150
|
+
data_path, artifact="implementation option data"
|
|
151
|
+
)
|
|
152
|
+
except JsonBoundaryError as exc:
|
|
146
153
|
raise DirectionSelectionError(f"data JSON is invalid: {data_path}") from exc
|
|
147
|
-
|
|
148
|
-
raise DirectionSelectionError("data JSON must contain an object")
|
|
149
|
-
return data
|
|
154
|
+
return snapshot.value, snapshot.raw_bytes
|
|
150
155
|
|
|
151
156
|
|
|
152
157
|
def _validate_report_identity(data: Mapping[str, Any], expected_task_key: str) -> None:
|
|
@@ -342,8 +347,7 @@ def resolve_selected_direction(
|
|
|
342
347
|
task_root, seq = _selection_layout(report)
|
|
343
348
|
report = validate_task_artifact_path(report, task_root, "report")
|
|
344
349
|
data_path = validate_task_artifact_path(data_path, task_root, "data")
|
|
345
|
-
data_bytes = data_path
|
|
346
|
-
data = _parse_data(data_path, data_bytes)
|
|
350
|
+
data, data_bytes = _parse_data(data_path)
|
|
347
351
|
_validate_report_identity(data, expected_task_key)
|
|
348
352
|
_validate_schema(data)
|
|
349
353
|
selection = data.get("implementationOptionSelection")
|
|
@@ -406,9 +410,10 @@ def write_selected_direction_snapshot(
|
|
|
406
410
|
"requirementCoverage": list(direction.requirement_coverage),
|
|
407
411
|
"planningInvariants": list(direction.planning_invariants),
|
|
408
412
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
413
|
+
write_owned_object_atomic(
|
|
414
|
+
destination,
|
|
415
|
+
payload,
|
|
416
|
+
artifact="selected implementation direction",
|
|
412
417
|
)
|
|
413
418
|
return destination
|
|
414
419
|
|
|
@@ -447,13 +452,16 @@ def _load_selected_direction_snapshot(
|
|
|
447
452
|
task_root = path.parent.parent
|
|
448
453
|
try:
|
|
449
454
|
path = validate_task_artifact_path(path, task_root, "selected direction snapshot")
|
|
450
|
-
|
|
451
|
-
|
|
455
|
+
loaded = load_owned_object_snapshot(
|
|
456
|
+
path, artifact="selected direction snapshot"
|
|
457
|
+
)
|
|
458
|
+
content = loaded.raw_bytes
|
|
459
|
+
data = loaded.value
|
|
452
460
|
except DirectionSelectionError as exc:
|
|
453
461
|
return None, [str(exc)]
|
|
454
462
|
except OSError as exc:
|
|
455
463
|
return None, [f"selected direction snapshot is unreadable: {exc}"]
|
|
456
|
-
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
464
|
+
except (UnicodeError, json.JSONDecodeError, JsonBoundaryError) as exc:
|
|
457
465
|
return None, [f"selected direction snapshot JSON is invalid: {exc}"]
|
|
458
466
|
if not isinstance(data, Mapping):
|
|
459
467
|
return None, ["selected direction snapshot JSON must contain an object"]
|
|
@@ -15,6 +15,7 @@ from . import next_phase
|
|
|
15
15
|
from .paths import RunRef, task_manifest_file
|
|
16
16
|
from .stage_map import StageMapError, load_task_stage_map
|
|
17
17
|
from .workflow import PHASE_SEQUENCE
|
|
18
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@dataclass(frozen=True)
|
|
@@ -114,8 +115,8 @@ def _load_json(path: Path) -> dict[str, Any]:
|
|
|
114
115
|
if not path.is_file():
|
|
115
116
|
return {}
|
|
116
117
|
try:
|
|
117
|
-
data =
|
|
118
|
-
except
|
|
118
|
+
data = load_owned_object(path, artifact="implementation outcome")
|
|
119
|
+
except JsonBoundaryError:
|
|
119
120
|
return {}
|
|
120
121
|
return data if isinstance(data, dict) else {}
|
|
121
122
|
|
|
@@ -224,8 +225,4 @@ def _update_task_catalog(project_root: Path, manifest: dict[str, Any]) -> bool:
|
|
|
224
225
|
|
|
225
226
|
|
|
226
227
|
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
227
|
-
path
|
|
228
|
-
path.write_text(
|
|
229
|
-
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
230
|
-
encoding="utf-8",
|
|
231
|
-
)
|
|
228
|
+
write_owned_object_atomic(path, payload, artifact="implementation outcome")
|
|
@@ -7,9 +7,16 @@ import json
|
|
|
7
7
|
import re
|
|
8
8
|
import sys
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from .convergence_store import write_json_atomic
|
|
13
|
+
from .final_report_schema import load_schema_version
|
|
14
|
+
from .report_narrative import parse_narrative, writer_owned_data
|
|
15
|
+
from .json_boundary import load_owned_object
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
PREP_VERDICT_RE = re.compile(r"^P-Prep-S([1-9][0-9]*)-")
|
|
19
|
+
STEP_VERDICT_RE = re.compile(r"^P-Step-([1-9][0-9]*)\.")
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
class CarryError(Exception):
|
|
@@ -290,6 +297,78 @@ def merge_carried_forward(
|
|
|
290
297
|
return cur
|
|
291
298
|
|
|
292
299
|
|
|
300
|
+
def _plan_item_stage(item: Mapping[str, Any]) -> int | None:
|
|
301
|
+
item_id = str(item.get("id") or "")
|
|
302
|
+
for pattern in (PREP_VERDICT_RE, STEP_VERDICT_RE):
|
|
303
|
+
match = pattern.match(item_id)
|
|
304
|
+
if match:
|
|
305
|
+
return int(match.group(1))
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _writer_stage_rows(data: Mapping[str, Any], *, snapshot: str) -> dict[int, dict]:
|
|
310
|
+
planning = _planning(dict(writer_owned_data(data)))
|
|
311
|
+
return _stage_rows(planning, snapshot=snapshot)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def merge_v3_plan_state(
|
|
315
|
+
prev: dict[str, Any],
|
|
316
|
+
narrative: dict[str, Any],
|
|
317
|
+
state: dict[str, Any],
|
|
318
|
+
*,
|
|
319
|
+
prev_seq: str,
|
|
320
|
+
carry_stages: set[int],
|
|
321
|
+
reverify_stages: set[int],
|
|
322
|
+
) -> dict[str, Any]:
|
|
323
|
+
"""이전 판정 중 이월 스테이지 소유분만 v3 수렴 상태에 복사한다."""
|
|
324
|
+
prev_stages = _writer_stage_rows(prev, snapshot="prior")
|
|
325
|
+
cur_stages = _stage_rows(_planning(narrative), snapshot="current")
|
|
326
|
+
_validate_stage_sets(prev_stages, cur_stages, carry_stages, reverify_stages)
|
|
327
|
+
for stage in sorted(carry_stages):
|
|
328
|
+
current = cur_stages.get(stage)
|
|
329
|
+
if current is None:
|
|
330
|
+
raise CarryError(f"carry stage {stage} is missing from current narrative")
|
|
331
|
+
if _canonical(prev_stages[stage]) != _canonical(current):
|
|
332
|
+
raise CarryError(f"carry stage {stage} changed in current narrative")
|
|
333
|
+
|
|
334
|
+
prev_items = _indexed_rows(
|
|
335
|
+
_planning(prev).get("planBodyVerification", {}).get("planItems", []),
|
|
336
|
+
key="id",
|
|
337
|
+
label="prior planItems",
|
|
338
|
+
)
|
|
339
|
+
state_items = _indexed_rows(
|
|
340
|
+
state.get("planBodyVerification", {}).get("planItems", []),
|
|
341
|
+
key="id",
|
|
342
|
+
label="current state planItems",
|
|
343
|
+
)
|
|
344
|
+
for item_id, current in state_items.items():
|
|
345
|
+
stage = _plan_item_stage(current)
|
|
346
|
+
if stage not in carry_stages:
|
|
347
|
+
continue
|
|
348
|
+
previous = prev_items.get(item_id)
|
|
349
|
+
if previous is None:
|
|
350
|
+
raise CarryError(f"carry-owned plan item {item_id} is missing from prior report")
|
|
351
|
+
for field in ("subject", "sourceSection"):
|
|
352
|
+
if current.get(field) != previous.get(field):
|
|
353
|
+
raise CarryError(f"carry-owned plan item {item_id} changed {field}")
|
|
354
|
+
carried = copy.deepcopy(previous)
|
|
355
|
+
carried["carriedForwardFromSeq"] = str(prev_seq)
|
|
356
|
+
state_items[item_id] = carried
|
|
357
|
+
|
|
358
|
+
prior_carried_ids = {
|
|
359
|
+
item_id
|
|
360
|
+
for item_id, item in prev_items.items()
|
|
361
|
+
if _plan_item_stage(item) in carry_stages
|
|
362
|
+
}
|
|
363
|
+
missing = sorted(prior_carried_ids - set(state_items))
|
|
364
|
+
if missing:
|
|
365
|
+
raise CarryError(f"current state omitted carry-owned plan items: {missing}")
|
|
366
|
+
state["planBodyVerification"]["planItems"] = [
|
|
367
|
+
state_items[item_id] for item_id in sorted(state_items)
|
|
368
|
+
]
|
|
369
|
+
return state
|
|
370
|
+
|
|
371
|
+
|
|
293
372
|
def _parse_stage_csv(value: str | None, *, option: str) -> set[int] | None:
|
|
294
373
|
if value is None:
|
|
295
374
|
return None
|
|
@@ -306,7 +385,7 @@ def _load_data(path: str, *, option: str) -> dict:
|
|
|
306
385
|
the documented `carry refused:` refusal it knows how to fall back from.
|
|
307
386
|
"""
|
|
308
387
|
try:
|
|
309
|
-
data =
|
|
388
|
+
data = load_owned_object(Path(path), artifact="incremental carry report")
|
|
310
389
|
except (OSError, ValueError) as exc:
|
|
311
390
|
raise CarryError(f"{option} could not be read: {exc}") from exc
|
|
312
391
|
if not isinstance(data, dict):
|
|
@@ -317,33 +396,67 @@ def _load_data(path: str, *, option: str) -> dict:
|
|
|
317
396
|
def main(argv: list[str]) -> int:
|
|
318
397
|
ap = argparse.ArgumentParser(prog="okstra incremental-carry")
|
|
319
398
|
ap.add_argument("--prev-data", required=True, help="prior run final-report data.json")
|
|
320
|
-
ap.
|
|
399
|
+
current = ap.add_mutually_exclusive_group(required=True)
|
|
400
|
+
current.add_argument("--cur-data", help="historical v2 current data.json")
|
|
401
|
+
current.add_argument("--cur-narrative", help="contract v3 current narrative Markdown")
|
|
402
|
+
ap.add_argument("--state", help="contract v3 convergence-owned plan state")
|
|
321
403
|
ap.add_argument("--prev-seq", required=True, help="prior run seq, tagged onto carried items")
|
|
322
404
|
ap.add_argument("--carry-stages", default=None, help="comma-separated carried stage numbers")
|
|
323
405
|
ap.add_argument("--reverify-stages", default=None, help="comma-separated reverified stage numbers")
|
|
324
|
-
ap.add_argument("--out",
|
|
406
|
+
ap.add_argument("--out", help="historical v2 merged data.json path")
|
|
407
|
+
ap.add_argument("--out-state", help="contract v3 merged plan state path")
|
|
325
408
|
args = ap.parse_args(argv)
|
|
326
409
|
|
|
327
410
|
try:
|
|
328
411
|
prev = _load_data(args.prev_data, option="--prev-data")
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
cur,
|
|
333
|
-
prev_seq=args.prev_seq,
|
|
334
|
-
carry_stages=_parse_stage_csv(args.carry_stages, option="--carry-stages"),
|
|
335
|
-
reverify_stages=_parse_stage_csv(
|
|
336
|
-
args.reverify_stages, option="--reverify-stages",
|
|
337
|
-
),
|
|
412
|
+
carry_stages = _parse_stage_csv(args.carry_stages, option="--carry-stages")
|
|
413
|
+
reverify_stages = _parse_stage_csv(
|
|
414
|
+
args.reverify_stages, option="--reverify-stages",
|
|
338
415
|
)
|
|
339
|
-
|
|
416
|
+
if args.cur_narrative:
|
|
417
|
+
if not args.state or not args.out_state or args.out:
|
|
418
|
+
raise CarryError(
|
|
419
|
+
"--cur-narrative requires --state and --out-state, and forbids --out"
|
|
420
|
+
)
|
|
421
|
+
if carry_stages is None or reverify_stages is None:
|
|
422
|
+
raise CarryError("contract v3 carry requires both stage sets")
|
|
423
|
+
try:
|
|
424
|
+
narrative = parse_narrative(
|
|
425
|
+
Path(args.cur_narrative).read_text(encoding="utf-8"),
|
|
426
|
+
load_schema_version("3.0"),
|
|
427
|
+
)
|
|
428
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
429
|
+
raise CarryError(f"--cur-narrative could not be read: {exc}") from exc
|
|
430
|
+
state = _load_data(args.state, option="--state")
|
|
431
|
+
merged = merge_v3_plan_state(
|
|
432
|
+
prev,
|
|
433
|
+
narrative,
|
|
434
|
+
state,
|
|
435
|
+
prev_seq=args.prev_seq,
|
|
436
|
+
carry_stages=carry_stages,
|
|
437
|
+
reverify_stages=reverify_stages,
|
|
438
|
+
)
|
|
439
|
+
output = Path(args.out_state)
|
|
440
|
+
else:
|
|
441
|
+
if not args.out or args.out_state or args.state:
|
|
442
|
+
raise CarryError(
|
|
443
|
+
"--cur-data requires --out, and forbids --state and --out-state"
|
|
444
|
+
)
|
|
445
|
+
cur = _load_data(args.cur_data, option="--cur-data")
|
|
446
|
+
merged = merge_carried_forward(
|
|
447
|
+
prev,
|
|
448
|
+
cur,
|
|
449
|
+
prev_seq=args.prev_seq,
|
|
450
|
+
carry_stages=carry_stages,
|
|
451
|
+
reverify_stages=reverify_stages,
|
|
452
|
+
)
|
|
453
|
+
output = Path(args.out)
|
|
454
|
+
except (CarryError, KeyError, TypeError) as exc:
|
|
340
455
|
print(f"carry refused: {exc}", file=sys.stderr)
|
|
341
456
|
return 1
|
|
342
457
|
|
|
343
458
|
try:
|
|
344
|
-
|
|
345
|
-
json.dumps(merged, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
346
|
-
)
|
|
459
|
+
write_json_atomic(output, merged)
|
|
347
460
|
except OSError as exc:
|
|
348
461
|
print(f"carry refused: --out could not be written: {exc}", file=sys.stderr)
|
|
349
462
|
return 1
|
|
@@ -354,7 +467,7 @@ def main(argv: list[str]) -> int:
|
|
|
354
467
|
.get("planItems", [])
|
|
355
468
|
if item.get("carriedForwardFromSeq") == str(args.prev_seq)
|
|
356
469
|
)
|
|
357
|
-
print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {
|
|
470
|
+
print(f"merged {carried} carried-forward plan item(s) from seq {args.prev_seq} -> {output}")
|
|
358
471
|
return 0
|
|
359
472
|
|
|
360
473
|
|
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from okstra_ctl.final_report_paths import final_report_data_path
|
|
19
19
|
from okstra_ctl.stage_citations import cited_stage_numbers
|
|
20
20
|
from okstra_ctl.stage_targets import downstream_stage_closure
|
|
21
|
+
from okstra_ctl.json_boundary import load_owned_object
|
|
21
22
|
|
|
22
23
|
CUTOFF_RATIO = 0.5
|
|
23
24
|
|
|
@@ -207,7 +208,10 @@ def stages_for_clarification(data: dict, clarification_id: str) -> set[int]:
|
|
|
207
208
|
)
|
|
208
209
|
stages = _stages_blocked_on(planning.get("requirementCoverage"), clarification_id)
|
|
209
210
|
for item in plan_items if isinstance(plan_items, list) else []:
|
|
210
|
-
if isinstance(item, dict) and
|
|
211
|
+
if isinstance(item, dict) and (
|
|
212
|
+
item.get("clarificationId") == clarification_id
|
|
213
|
+
or clarification_id in (item.get("clarificationRefs") or [])
|
|
214
|
+
):
|
|
211
215
|
stages |= _plan_item_stages(item)
|
|
212
216
|
return stages
|
|
213
217
|
|
|
@@ -272,7 +276,7 @@ def preview_link_availability_for_report(
|
|
|
272
276
|
"reason": f"prior report has no data sibling at {data_path.name}",
|
|
273
277
|
}
|
|
274
278
|
try:
|
|
275
|
-
data =
|
|
279
|
+
data = load_owned_object(data_path, artifact="incremental scope report")
|
|
276
280
|
except (OSError, ValueError) as exc:
|
|
277
281
|
return {
|
|
278
282
|
"wouldForceFull": True,
|
|
@@ -329,7 +333,9 @@ def _preview_result(args) -> dict:
|
|
|
329
333
|
answer when the input cannot be read.
|
|
330
334
|
"""
|
|
331
335
|
try:
|
|
332
|
-
data =
|
|
336
|
+
data = load_owned_object(
|
|
337
|
+
Path(args.prev_data), artifact="incremental scope report"
|
|
338
|
+
)
|
|
333
339
|
answered = {
|
|
334
340
|
token.strip()
|
|
335
341
|
for token in args.answered_clarifications.split(",")
|
|
@@ -389,7 +395,9 @@ def main(argv: list[str]) -> int:
|
|
|
389
395
|
return 0
|
|
390
396
|
|
|
391
397
|
try:
|
|
392
|
-
data =
|
|
398
|
+
data = load_owned_object(
|
|
399
|
+
Path(args.prev_data), artifact="incremental scope report"
|
|
400
|
+
)
|
|
393
401
|
stages = parse_stage_graph(data)
|
|
394
402
|
impacted = {int(t.strip()) for t in args.impacted.split(",") if t.strip()}
|
|
395
403
|
prep_ids = {t.strip() for t in args.prep_items.split(",") if t.strip()}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
"""active/projects 인덱스 행 치환·기록."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import json
|
|
5
4
|
import os
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
from .ids import build_run_id
|
|
9
8
|
from .invocation import save_invocation
|
|
10
|
-
from .jsonl import
|
|
9
|
+
from .jsonl import (
|
|
10
|
+
append_jsonl, read_jsonl, remove_jsonl_row, rewrite_jsonl,
|
|
11
|
+
rotate_recent_if_needed,
|
|
12
|
+
)
|
|
11
13
|
from .project_meta import upsert_project_meta
|
|
14
|
+
from .json_boundary import load_owned_object, write_owned_object_atomic
|
|
12
15
|
from .run_index_row import build_run_index_row, slim_run_row
|
|
13
16
|
|
|
14
17
|
|
|
@@ -21,11 +24,7 @@ def _replace_or_append_row(path: Path, run_id: str, row: dict) -> None:
|
|
|
21
24
|
if r.get("runId") == run_id:
|
|
22
25
|
rows[i] = slim
|
|
23
26
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
-
|
|
25
|
-
with tmp.open("w") as f:
|
|
26
|
-
for rr in rows:
|
|
27
|
-
f.write(json.dumps(rr, separators=(",", ":")) + "\n")
|
|
28
|
-
os.replace(tmp, path)
|
|
27
|
+
rewrite_jsonl(path, rows)
|
|
29
28
|
return
|
|
30
29
|
append_jsonl(path, slim)
|
|
31
30
|
|
|
@@ -156,9 +155,7 @@ def remove_reservation(home: Path, *, project_id: str, task_group: str,
|
|
|
156
155
|
target = home / "projects" / project_id / "meta.json"
|
|
157
156
|
if not target.is_file():
|
|
158
157
|
return
|
|
159
|
-
meta =
|
|
158
|
+
meta = load_owned_object(target, artifact="project metadata")
|
|
160
159
|
meta["runCount"] = max(0, int(meta.get("runCount", 0)) - 1)
|
|
161
160
|
meta["activeCount"] = max(0, int(meta.get("activeCount", 0)) - 1)
|
|
162
|
-
|
|
163
|
-
tmp.write_text(json.dumps(meta, indent=2) + "\n")
|
|
164
|
-
os.replace(tmp, target)
|
|
161
|
+
write_owned_object_atomic(target, meta, artifact="project metadata")
|