okstra 0.183.2 → 0.184.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 +1 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +7 -4
- package/docs/project-structure-overview.md +1 -1
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +2 -3
- package/docs/task-process/error-analysis.md +3 -4
- package/docs/task-process/final-verification.md +2 -3
- package/docs/task-process/implementation-planning.md +2 -3
- package/docs/task-process/implementation.md +2 -3
- package/docs/task-process/release-handoff.md +3 -4
- package/docs/task-process/requirements-discovery.md +3 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +8 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/plan-body-verification.md +27 -19
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_implementation-executor.md +1 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +9 -11
- package/runtime/prompts/wizard/prompts.ko.json +7 -10
- package/runtime/python/okstra_ctl/conformance.py +37 -1
- package/runtime/python/okstra_ctl/incremental_scope.py +84 -39
- package/runtime/python/okstra_ctl/plan_items.py +410 -1
- package/runtime/python/okstra_ctl/plan_items_cli.py +346 -31
- package/runtime/python/okstra_ctl/render.py +4 -0
- package/runtime/python/okstra_ctl/wizard.py +39 -73
- package/runtime/schemas/final-report-v2.0.schema.json +12 -0
- package/runtime/schemas/final-report-v3.0.schema.json +12 -0
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/validators/validate-run.py +164 -66
- package/runtime/validators/validate_session_conformance.py +24 -5
|
@@ -78,7 +78,7 @@ also remain contract violations.
|
|
|
78
78
|
```
|
|
79
79
|
`overall` is exactly one of `PASS` / `FAIL` / `MISSING`. Writing the honest sidecar is mandatory whenever the script runs and on the exemption/waiver skip path. A missing `io`-only sidecar blocks; a missing external-advisory sidecar is reported as `ADVISORY` rather than accepted as hidden evidence.
|
|
80
80
|
- **Read-only command log.** Record the `runCommand` exact line + its exit code in the Read-only command log. Tier 3 external non-PASS evidence MUST remain visible with status `ADVISORY`. Unlike Tiers 1·2, a conformance script MAY mutate the **replica datastore** (exercising integrated state is its whole purpose) — but only the `qaEnv` replica target, never a shared/staging/prod store. The `runCommand` itself is still subject to the same source/lockfile mutation deny-list as Tier 2 (`--fix`, `npm install` without `ci`, etc.); a denied token aborts with `contract-violated`.
|
|
81
|
-
- **No manifest / no entry for this stage.** If the
|
|
81
|
+
- **No manifest / no entry for this stage.** If the approved plan declared `Conformance exemption:` for this stage, and the manifest is absent or has no matching `stageKey`, record `conformance: no manifest entry for <stageKey>` and proceed. If the approved plan declared `Conformance tests:` and the script file or matching entry is absent, that is a FAIL — do not treat it as a skip. **Enforced:** `validators/validate-run.py` `_validate_conformance`.
|
|
82
82
|
|
|
83
83
|
### Self-mock detection (changed test files)
|
|
84
84
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
roles:
|
|
5
5
|
- role: planner
|
|
6
6
|
min: 2
|
|
7
|
-
recommended:
|
|
7
|
+
recommended: 2
|
|
8
8
|
max: 5
|
|
9
9
|
duty: planning-worker
|
|
10
10
|
- role: critic
|
|
@@ -103,7 +103,7 @@ roles:
|
|
|
103
103
|
- Phase 5.5 finding convergence runs in **adversarial mode** for this phase (`convergence.adversarial=true`). Verifiers actively try to refute each worker finding (requirement gap / risk / plan item) by re-inspecting its cited evidence; the burden of proof sits on the claim. See `prompts/lead/convergence.md` §"Adversarial Verification Mode".
|
|
104
104
|
- §5.5.9 plan-body verification runs with an **adversarial posture** (`prompts/lead/plan-body-verification.md` §"Adversarial plan-body posture"): verifiers open and confirm every cited path / command and put the burden of proof on the plan. The gate threshold is majority-based for kinds `b`/`c`/`e`, but a single `DISAGREE` blocks on its own for the concrete, safety-critical kind `a` (path/symbol mismatch) — and `f` on `P-Req-*` items. `P-Var-*` items are excepted from the kind-`a` exception: a variation-point defect takes a majority. Rollback ordering (`d`) is advisory and never blocks the gate — a rollback is executed by a human, not by okstra's workers or verifiers. A majority also needs ≥2 participating votes, so a lone dissent whose peer returned a non-result does not block on a majority-gated kind (see that contract's §"Adversarial plan-body posture").
|
|
105
105
|
- **Incremental re-verification scope (clarification re-runs):** when the lead's `okstra incremental-scope` decision is `mode == "incremental"` (procedure in `prompts/launch.template.md` §"Clarification Response Carried In"), workers re-analyze ONLY the stages listed in `reverify_stages` (the downstream closure of the impacted stages). Workers MUST NOT re-open, re-score, or re-judge any stage in `carry_stages` — those stages' prior plan-item verdicts are carried forward verbatim, and a worker never overwrites a carried verdict with its own judgement. When the decision is `mode == "full"` (the default), every stage is re-analyzed as usual.
|
|
106
|
-
- **Single incremental-scope decision:** the lead calls `okstra incremental-scope`
|
|
106
|
+
- **Single incremental-scope decision:** the lead calls `okstra incremental-scope` once the inputs are complete, passing the answered `C-NNN` ids through `--answered-clarifications`, changed design-preparation IDs through `--prep-items`, and any lead-resolved stage numbers through `--impacted`; the CLI unions all three before applying the existing dependency closure and cutoff. The clarification ids are resolved to stages by the CLI from the prior report's own `planItems[].clarificationId` and `blocked C-NNN` coverage links — the lead does not map answers to stage numbers. An answer that changes the selected planning payload, Stage Map, or execution approach is not a local impact: pass `--full-reason`, which is the only structural path that still forces `mode == "full"`. A clarification id that traces to no stage returns `mode == "unresolved"` — ask the user for stage numbers and call again with `--impacted`; do not treat it as full and do not silently drop the id. Unknown PREP IDs or invalid `stageRefs` still return an explicit full decision instead of being guessed. When the wizard pin is `auto` and the CLI returns `mode == "incremental"`, keep it — do not upgrade to full. When the user pinned a scope at the wizard (`REVERIFY_SCOPE_MODE` / `REVERIFY_SCOPE_STAGES` in `prompts/launch.template.md` §"Clarification Response Carried In" step 0), that pin is an input to this same call — `full` supplies the `--full-reason`, and pinned stage numbers join `--impacted` — never a bypass of the CLI's closure and cutoff.
|
|
107
107
|
- **Stage-aware carry:** for an incremental decision, the report writer copies each `carry_stages` stage row unchanged into its narrative. After plan-item seeding, pass the decision's `carry_stages` and `reverify_stages` CSVs unchanged to `okstra incremental-carry --cur-narrative ... --state ... --out-state ...`. The helper rejects a changed or missing carried stage and copies only its prior `P-Step-*` / `P-Prep-*` verdicts into the convergence-owned state. Overlap, omissions, and canonical conflicts return `CarryError`. On that error, discard the partial state and run full re-verification.
|
|
108
108
|
{{INCLUDE:_coverage-critic.md}}
|
|
109
109
|
- Non-goals:
|
|
@@ -169,17 +169,15 @@ roles:
|
|
|
169
169
|
- **Clean-tree assertions use `okstra worktree-status --check-clean`.** A bare `git status --porcelain` is never empty there, so an assertion built on one fails on okstra's scaffolding rather than on the stage's work. The okstra command asks the same question over source paths only and exits 1 when dirty, so it stands alone as a step's assertion: `okstra worktree-status --check-clean`. Validator S13 rejects the bare form. Do not add a `git tag stage-<N>-exit` to the step — okstra writes that tag itself when it settles the stage, at the commit the carry evidence records, and a step that tags mid-stage puts it on an earlier commit.
|
|
170
170
|
- **Never read an `.okstra/` artifact back out of a git object.** `.okstra/**` is gitignored and never committed — the executor aborts a commit that stages an ignored path and the verifier reports a committed `.okstra` path as a branch defect — so `git cat-file -e <tag>:.okstra/…`, `git show <tag>:.okstra/…`, and every variant of that read can never resolve, at any tag, in any stage. A later stage that needs a QA artifact reads it from the working tree or receives it through the carry sidecar / verifier result; do not design a stage contract around one being reachable from a tag. Validator S12 rejects the read.
|
|
171
171
|
- **Per-stage conformance declaration (mandatory one line, in the stage section — same placement freedom as `TDD exemption:`):** the stage MUST carry exactly one of:
|
|
172
|
-
- `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — a Tier3 verification script
|
|
173
|
-
- `Conformance exemption: <reason>` — only for stages that touch no db/io/http/external surface, or where unit tests fully cover the increment. (If the eventual `implementation` diff actually touches one of those surfaces, `validate-run.py`'s diff-surface cross-check is BLOCKING — an exemption cannot hide a real db/io/http/external change.)
|
|
174
|
-
- **External QA outcome guideline:** after satisfying the
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
prerequisites, requirement IDs, and expected `QA-RESULT`. Okstra may start the
|
|
178
|
-
environment and run it automatically, but `FAIL`, missing evidence, or an
|
|
172
|
+
- `Conformance tests: stage-<N> — <task_root>/qa/scripts/stage-<N>.<ext> (requires=[db|io|http|external,...])` — declare that a Tier3 verification script will prove this stage's upstream requirements (brief / requirements-discovery / error-analysis / improvement-discovery → this stage's `Acceptance`) hold against **real** DB rows, real endpoints, or the real external API — NOT mocks. This phase emits the line and the `requires` set only. Do NOT write `<task_root>/qa/scripts/stage-<N>.*` and do NOT add a `runCommand` or `conformance-manifest.json` entry here — the matching `implementation` stage run creates the script file and the manifest `runCommand`. A plan that declares tests with no script file on disk is valid at this gate.
|
|
173
|
+
- `Conformance exemption: <reason>` — only for stages that touch no db/io/http/external surface, or where unit tests fully cover the increment. Exemption stays a planning declaration; do not move it to implementation. (If the eventual `implementation` diff actually touches one of those surfaces, `validate-run.py`'s diff-surface cross-check is BLOCKING — an exemption cannot hide a real db/io/http/external change.)
|
|
174
|
+
- **External QA outcome guideline:** after satisfying the S11 declaration above, a line whose `requires` contains
|
|
175
|
+
`db`, `http`, or `external` should name those capabilities here so the later `runCommand` can be written against them.
|
|
176
|
+
Okstra may start the environment and run it automatically, but `FAIL`, missing evidence, or an
|
|
179
177
|
unavailable environment is a user-owned follow-up, never a plan approval or
|
|
180
178
|
later run blocker. `requires=[]` and `requires=[io]` remain blocking.
|
|
181
179
|
Remote IO should also declare `external`.
|
|
182
|
-
|
|
180
|
+
Layout split (the implementer writes these, not this phase): executable scripts (conformance + any real-IO test) live under `<task_root>/qa/scripts/`; data sidecars (`conformance-manifest.json`, `result-*.json`) stay at the `qa/` root. This declaration is enforced at four layers: `validators/validate-implementation-plan-stages.py` check **S11** forces every stage to carry one of the two lines; at the planning boundary `validators/validate-run.py` `_validate_planning_conformance_declared` accepts a well-formed `Conformance tests:` line even when the script file and manifest entry are absent (malformed `requires` still fails); the matching `implementation` stage run that inherited `Conformance tests:` fails closed when the script file is missing (`_validate_conformance`); and the manifest JSON structure — including each entry's `script` living under `qa/scripts/` and a `runCommand` that does not change cwd — is enforced by `validate_conformance_manifest` when the implementer writes the entry.
|
|
183
181
|
- `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
|
|
184
182
|
- `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
|
|
185
183
|
- **Dependency precondition (stages that run the project toolchain).** The planning worktree is created without installed dependencies, so a stage whose steps call `npm` / `yarn` / `pytest` / `cargo` / equivalent cannot have those commands succeed at plan time — they exit `127`, not RED/GREEN. Declare the install **once** as a `phase: pre` row in `### Validation Checklist` (e.g. `VC-008 — the implementation run's stage worktree has workspace dependencies installed`) and have every such stage's `Stage Validation` cite that `VC-NNN` in its `pre:` line. Do not repeat the install commands per stage, and do not silently assume the tooling is present: a plan that never states the precondition produces steps whose commands never resolve, which the §5.5.9 round then reports as unverifiable. **Enforced (advisory):** `validators/validate-run.py` `_detect_missing_dependency_precondition` warns when a toolchain-invoking stage cites no `VC-NNN`, or cites one that is not `phase: pre`. Whether the cited row genuinely covers dependencies is a §5.5.9 judgement, not a machine check. Detection uses the token allowlist in `scripts/okstra_ctl/build_tools.py`; a project overrides it with `buildToolTokens` in `.okstra/project.json`.
|
|
@@ -263,6 +261,6 @@ roles:
|
|
|
263
261
|
10. **Decision-draft materialization check** — when `decisionDrafts` is non-empty, confirm as a reviewer which stage's stepwise order contains the matching materialization step (creating `.okstra/decisions/<NNNN>-<slug>.md`) and that the number of drafts corresponds 1:1 with the materialization steps. The validator only checks the *existence* of the step, so the `<NNNN>-<slug>` correctness and count correspondence are the self-review's responsibility.
|
|
264
262
|
11. **Variation-point & seam check** — read `variationPointAnalysis` as a skeptic. Is `hasMultipleImplementations` honest against the brief and the sibling code you inspected during pre-planning, or was `false` chosen because it is the cheaper field to fill? For every point with `extract: true`, confirm the `extractionDecision` names a real interface (a `port` for a hexagonal project, not a shared helper) and a `coveredBy` stage that exists in the Stage Map — an interface no stage builds is a decision nobody executes. Then read the chosen realization's `testSeams`: each `injectedAs` must name a construction or wiring point a test can actually substitute at, not a symbol the test would have to re-implement — a seam nothing can be injected into leaves the executor writing self-mocks. An empty `testSeams` array is only acceptable when you can defend it in one sentence; the validator accepts it either way, so this is the check that catches an unfilled field posing as a decision.
|
|
265
263
|
12. **Approval blast-radius check (BLOCKING).** Every approval clarification must be reachable from `planItems[].clarificationRefs[]` or a requirement-coverage blocker. Report assembly derives plan-item links from activity `clarificationRefs[]` plus `planItemIds[]`; `okstra incremental-scope` reads the resulting reverse links.
|
|
266
|
-
- **The link must resolve to a stage, not merely exist.** `incremental-scope` reads the stage number out of a `P-Step-<stage>.<step>` / `P-Prep-S<stage>-<kind>` plan-item id, or out of a `Stage N` citation in the blocked coverage row's `coveredBy`. Every other plan-item prefix (`P-Dir-1`, `P-Req-*`, `P-Val-*`, `P-Opt-*`, `P-Dep-*`, `P-Rb-*`) carries no stage, so a blocker linked only that way MUST also have its coverage row cite the stage in `coveredBy`. Writing the blocked row's `coveredBy` as prose with no `Stage N` in it — `No stage.`, `Partly covered — …` — satisfies nothing: the row passes the link check and the re-run
|
|
264
|
+
- **The link must resolve to a stage, not merely exist.** `incremental-scope` reads the stage number out of a `P-Step-<stage>.<step>` / `P-Prep-S<stage>-<kind>` plan-item id, or out of a `Stage N` citation in the blocked coverage row's `coveredBy`. Every other plan-item prefix (`P-Dir-1`, `P-Req-*`, `P-Val-*`, `P-Opt-*`, `P-Dep-*`, `P-Rb-*`) carries no stage, so a blocker linked only that way MUST also have its coverage row cite the stage in `coveredBy`. Writing the blocked row's `coveredBy` as prose with no `Stage N` in it — `No stage.`, `Partly covered — …` — satisfies nothing: the row passes the link check and the next re-run cannot place the answer without asking for stage numbers.
|
|
267
265
|
- What to write when no stage covers the requirement yet: name the stage the answer will change, not the stage that satisfies the requirement today. A `Blocks=approval` row is admissible only when, absent an answer, `implementation` would produce wrong or unsafe code (see the admissibility rule above) — so some stage's code is at stake by construction. If you genuinely cannot name one, the row fails the admissibility test and belongs in `## 5. Missing Information and Risks` with `Blocks=none`, not in the approval gate.
|
|
268
266
|
**Enforced:** `validators/validate-run.py` `_validate_approval_clarification_backtrace` — one failure for a missing link, a separate one for a link that resolves to no stage.
|
|
@@ -408,6 +408,7 @@
|
|
|
408
408
|
},
|
|
409
409
|
"reverify_scope_pick": {
|
|
410
410
|
"label": "재검증 범위를 어떻게 할까요? (답변한 항목이 모두 직전 리포트의 stage 에 연결돼 있어 범위를 좁힐 수 있는 상태입니다 — 최종 확정은 run 시점 base-ref 비교)",
|
|
411
|
+
"label_unlinked": "답변한 항목 중 직전 리포트의 stage 에 연결되지 않은 것이 있습니다. 다시 볼 stage 번호를 지정하거나 전체 재검증을 고르세요. 연결되지 않은 id 가 재실행 전체를 full 로 만들지는 않습니다.",
|
|
411
412
|
"echo_template": "reverify-scope: {value}",
|
|
412
413
|
"options": {
|
|
413
414
|
"auto": "관련 stage 만 (추천) — 답변이 닿는 stage 와 그 하위만 다시 검증하고 나머지는 직전 판정을 그대로 이월",
|
|
@@ -417,6 +418,9 @@
|
|
|
417
418
|
"echo_suffixes": {
|
|
418
419
|
"auto": "reverify-scope: auto (좁힐 수 있으면 좁힘)",
|
|
419
420
|
"full": "reverify-scope: full (전체 재검증)"
|
|
421
|
+
},
|
|
422
|
+
"errors": {
|
|
423
|
+
"unlinked_auto": "직전 리포트의 stage 에 연결되지 않은 답변이 있어 자동 판정을 쓸 수 없습니다: {ids}. stage 번호를 지정하거나 전체 재검증을 고르세요."
|
|
420
424
|
}
|
|
421
425
|
},
|
|
422
426
|
"reverify_scope_stages": {
|
|
@@ -428,7 +432,8 @@
|
|
|
428
432
|
"errors": {
|
|
429
433
|
"not_a_number": "stage 번호는 정수여야 합니다: {token}",
|
|
430
434
|
"unknown_stage": "직전 리포트의 Stage Map 에 없는 stage 입니다: {stages} (Stage Map 의 번호: {known})",
|
|
431
|
-
"no_stage_map": "직전 리포트에서 Stage Map 을 읽을 수 없어 stage 번호를 검증할 수 없습니다: {reason}"
|
|
435
|
+
"no_stage_map": "직전 리포트에서 Stage Map 을 읽을 수 없어 stage 번호를 검증할 수 없습니다: {reason}",
|
|
436
|
+
"unlinked_empty": "직전 리포트의 stage 에 연결되지 않은 답변이 있어 빈 입력(자동)을 쓸 수 없습니다: {ids}. stage 번호를 입력하세요."
|
|
432
437
|
}
|
|
433
438
|
},
|
|
434
439
|
"pr_template_pick": {
|
|
@@ -480,14 +485,6 @@
|
|
|
480
485
|
"no": "아니오 — 단계별로 다시 입력"
|
|
481
486
|
}
|
|
482
487
|
},
|
|
483
|
-
"leader_session": {
|
|
484
|
-
"label": "리더는 현재 세션 모델을 씁니다 (읽기 전용): {model_ref}{effort_suffix}",
|
|
485
|
-
"echo_template": "leader-session: {value}",
|
|
486
|
-
"options": {
|
|
487
|
-
"continue": "계속"
|
|
488
|
-
},
|
|
489
|
-
"effort_suffix": " · effort {effort}"
|
|
490
|
-
},
|
|
491
488
|
"role_count": {
|
|
492
489
|
"label": "{role} 역할 인스턴스 수를 선택하세요 ({minimum}..{maximum}, 적정 {default})",
|
|
493
490
|
"echo_template": "role-count: {value}",
|
|
@@ -636,7 +633,7 @@
|
|
|
636
633
|
"clarification_sidecars_attached": " user-responses: 사이드카 {files}개 · 답변 {count}개 함께 첨부 — {ids}",
|
|
637
634
|
"clarification_sidecars_none_parsed": "답변으로 셀 항목 없음 (reframe 등)",
|
|
638
635
|
"reverify_scope_incremental": " reverify-scope: incremental 가능 — 답변된 항목이 모두 직전 리포트의 stage 에 연결됨 (최종 확정은 run 시점 base-ref 비교)",
|
|
639
|
-
"reverify_scope_unlinked": " reverify-scope:
|
|
636
|
+
"reverify_scope_unlinked": " reverify-scope: stage 번호 필요 — {ids} 이(가) 직전 리포트의 어느 stage 에도 연결되지 않음 (자동 판정 불가; full 로 강제되지는 않음)",
|
|
640
637
|
"reverify_scope_full": " reverify-scope: full 예상 — {reason}\n (좁히지 못하는 것은 재검증 범위이지 답변이 아닙니다 — 답변은 모두 carry-in 되어 반영되고, 대신 stage 전체를 다시 검증합니다)",
|
|
641
638
|
"reverify_scope_user_full": " reverify-scope: full (사용자 지정 — 전체 재검증)",
|
|
642
639
|
"reverify_scope_user_stages": " reverify-scope: stage {stages} 재검증 지정 (사용자 지정 — 하위 stage 포함, 나머지는 직전 판정 이월)",
|
|
@@ -5,14 +5,17 @@ implementation/final-verification 의 verifier 는 stage 별 conformance 스크
|
|
|
5
5
|
|
|
6
6
|
1. `conformance-manifest.json` 구조 검증 (`validate_conformance_manifest`).
|
|
7
7
|
2. 스크립트 stdout 의 `QA-RESULT` 마커 파싱 (`parse_qa_result`).
|
|
8
|
+
3. 선언된 script 파일 부재 (`missing_declared_scripts`) — 구현/최종검증만.
|
|
8
9
|
|
|
9
10
|
스크립트 실행/게이트 강제는 verifier prompt 와 validators/validate-run.py 가 담당한다.
|
|
11
|
+
계획 단계는 선언만 내고 이 모듈의 파일 존재 검사를 호출하지 않는다.
|
|
10
12
|
"""
|
|
11
13
|
from __future__ import annotations
|
|
12
14
|
|
|
13
15
|
import fnmatch
|
|
14
16
|
import re
|
|
15
17
|
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
16
19
|
|
|
17
20
|
# 셸에서 프로세스의 cwd 를 바꾸는 명령. verifier 가 워크트리 cwd 에서 실행하는
|
|
18
21
|
# 계약이 이것들로 무력화된다.
|
|
@@ -45,6 +48,39 @@ def normalize_conformance_script(script: str) -> str:
|
|
|
45
48
|
return script[len(prefix):] if script.startswith(prefix) else script
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
def missing_declared_scripts(entries: object, task_root: Path) -> list[str]:
|
|
52
|
+
"""승인 계획이 가리키는 script 파일이 task_root 아래에 없으면 오류.
|
|
53
|
+
|
|
54
|
+
계획 단계는 호출하지 않는다. 스크립트와 runCommand 는 매칭 implementation
|
|
55
|
+
stage 가 만들고, 구현/최종검증 게이트만 이 함수로 부재를 닫힌 실패로 본다.
|
|
56
|
+
"""
|
|
57
|
+
errors: list[str] = []
|
|
58
|
+
if not isinstance(entries, list):
|
|
59
|
+
return errors
|
|
60
|
+
root = task_root.resolve()
|
|
61
|
+
for entry in entries:
|
|
62
|
+
if not isinstance(entry, dict):
|
|
63
|
+
continue
|
|
64
|
+
script = entry.get("script")
|
|
65
|
+
if not isinstance(script, str) or not script.strip():
|
|
66
|
+
continue
|
|
67
|
+
relative = normalize_conformance_script(script)
|
|
68
|
+
candidate = (root / relative).resolve()
|
|
69
|
+
stage_number = str(entry.get("stageKey") or "").rsplit("-stage-", 1)[-1]
|
|
70
|
+
try:
|
|
71
|
+
candidate.relative_to(root)
|
|
72
|
+
except ValueError:
|
|
73
|
+
errors.append(
|
|
74
|
+
f"stage {stage_number} declared script escapes task root: {relative}"
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
if not candidate.is_file():
|
|
78
|
+
errors.append(
|
|
79
|
+
f"stage {stage_number} declared script is missing: {relative}"
|
|
80
|
+
)
|
|
81
|
+
return errors
|
|
82
|
+
|
|
83
|
+
|
|
48
84
|
def parse_conformance_tests(value: object) -> tuple[str, frozenset[str]] | None:
|
|
49
85
|
"""stage 선언의 `<script> (requires=[cap,...])` 를 파싱. 형식이 아니면 None."""
|
|
50
86
|
if not isinstance(value, str):
|
|
@@ -156,7 +192,7 @@ def _check_entry(entry: object, idx: int, errors: list[str]) -> None:
|
|
|
156
192
|
_check_nonempty_str(entry.get("stageKey"), f"{path}.stageKey", errors)
|
|
157
193
|
_check_nonempty_str(entry.get("script"), f"{path}.script", errors)
|
|
158
194
|
script = entry.get("script")
|
|
159
|
-
# 실행 스크립트는 qa/scripts/ 하위 격리가 계약(implementation
|
|
195
|
+
# 실행 스크립트는 qa/scripts/ 하위 격리가 계약(implementation 이 파일을 씀);
|
|
160
196
|
# qa/ 루트는 manifest·result-*.json 데이터 사이드카 전용이다.
|
|
161
197
|
if isinstance(script, str) and script.strip() and "qa/scripts/" not in script:
|
|
162
198
|
errors.append(f"{path}.script must live under the task qa scripts dir (qa/scripts/), got {script!r}")
|
|
@@ -28,6 +28,11 @@ CUTOFF_RATIO = 0.5
|
|
|
28
28
|
# either a structural judgement or a mapping the lead never made.
|
|
29
29
|
DECLARED_FULL_PREFIX = "declared structural change:"
|
|
30
30
|
|
|
31
|
+
# CLI-to-lead signal only: an answered id traced to no stage and the user has
|
|
32
|
+
# not named `--impacted` stages. Never recorded as incrementalDecision (schema
|
|
33
|
+
# enum is incremental|full). The lead asks for stage numbers and calls again.
|
|
34
|
+
UNRESOLVED_MODE = "unresolved"
|
|
35
|
+
|
|
31
36
|
# `P-Step-<stage>.<step>` and `P-Prep-S<stage>-<kind>` carry their stage in the
|
|
32
37
|
# id itself. Every other prefix (`P-Opt`, `P-Dep`, `P-Val`, `P-Rb`, `P-Req`) is
|
|
33
38
|
# numbered by position in its own array, so its stage is only recoverable from
|
|
@@ -37,7 +42,7 @@ _STRUCTURAL_STAGE_IN_ID_RE = re.compile(r"^P-(?:Step-(\d+)\.\d+|Prep-S(\d+)-)")
|
|
|
37
42
|
|
|
38
43
|
@dataclass
|
|
39
44
|
class IncrementalDecision:
|
|
40
|
-
mode: str # "incremental" | "full"
|
|
45
|
+
mode: str # "incremental" | "full" | "unresolved"
|
|
41
46
|
reverify_stages: list[int]
|
|
42
47
|
carry_stages: list[int]
|
|
43
48
|
reason: str
|
|
@@ -167,6 +172,25 @@ def _stages_blocked_on(coverage: object, clarification_id: str) -> set[int]:
|
|
|
167
172
|
return stages
|
|
168
173
|
|
|
169
174
|
|
|
175
|
+
def partition_clarification_stages(
|
|
176
|
+
data: dict, clarification_ids: set[str]
|
|
177
|
+
) -> tuple[set[int], list[str]]:
|
|
178
|
+
"""연결된 stage 번호와, 아무 stage 에도 안 닿는 id.
|
|
179
|
+
|
|
180
|
+
preview 와 판정 경로가 같은 분할을 쓴다. unlinked 는 full 강등이 아니라
|
|
181
|
+
`--impacted` 를 받거나 `--full-reason` 을 받는 분기이다.
|
|
182
|
+
"""
|
|
183
|
+
impacted: set[int] = set()
|
|
184
|
+
unlinked: list[str] = []
|
|
185
|
+
for clarification_id in sorted(clarification_ids):
|
|
186
|
+
stages = stages_for_clarification(data, clarification_id)
|
|
187
|
+
if stages:
|
|
188
|
+
impacted |= stages
|
|
189
|
+
else:
|
|
190
|
+
unlinked.append(clarification_id)
|
|
191
|
+
return impacted, unlinked
|
|
192
|
+
|
|
193
|
+
|
|
170
194
|
def clarification_impacted_stages(
|
|
171
195
|
data: dict, clarification_ids: set[str]
|
|
172
196
|
) -> set[int]:
|
|
@@ -179,16 +203,15 @@ def clarification_impacted_stages(
|
|
|
179
203
|
|
|
180
204
|
Raises when any id resolves to no stage — a partially-resolved set would
|
|
181
205
|
narrow the re-run past an answer whose blast radius nobody established.
|
|
206
|
+
The CLI decision path does not use this raise as a full fallback; it asks
|
|
207
|
+
for `--impacted` instead.
|
|
182
208
|
"""
|
|
183
|
-
impacted
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"(no plan item or coverage row cites one)"
|
|
190
|
-
)
|
|
191
|
-
impacted |= stages
|
|
209
|
+
impacted, unlinked = partition_clarification_stages(data, clarification_ids)
|
|
210
|
+
if unlinked:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"answered clarification {unlinked[0]} traces to no stage "
|
|
213
|
+
"(no plan item or coverage row cites one)"
|
|
214
|
+
)
|
|
192
215
|
return impacted
|
|
193
216
|
|
|
194
217
|
|
|
@@ -227,20 +250,19 @@ def preview_link_availability(data: dict, clarification_ids: set[str]) -> dict:
|
|
|
227
250
|
be shown while the run is still reshapeable.
|
|
228
251
|
|
|
229
252
|
`wouldForceFull: false` is therefore not a promise of `incremental` — it
|
|
230
|
-
says only that this half found nothing forcing full.
|
|
253
|
+
says only that this half found nothing forcing full. An unlinked id is
|
|
254
|
+
not such a thing: it cannot auto-narrow, and it does not force full.
|
|
231
255
|
"""
|
|
232
|
-
unlinked =
|
|
233
|
-
clarification_id
|
|
234
|
-
for clarification_id in clarification_ids
|
|
235
|
-
if not stages_for_clarification(data, clarification_id)
|
|
236
|
-
)
|
|
256
|
+
_, unlinked = partition_clarification_stages(data, clarification_ids)
|
|
237
257
|
if unlinked:
|
|
238
258
|
return {
|
|
239
|
-
"wouldForceFull":
|
|
259
|
+
"wouldForceFull": False,
|
|
240
260
|
"unlinkedIds": unlinked,
|
|
241
261
|
"reason": (
|
|
242
262
|
f"{', '.join(unlinked)} trace(s) to no stage in the prior report — "
|
|
243
|
-
"
|
|
263
|
+
"name those stages via --impacted, or declare a structural change "
|
|
264
|
+
"with --full-reason; an unlinked id does not force the whole re-run "
|
|
265
|
+
"to full"
|
|
244
266
|
),
|
|
245
267
|
}
|
|
246
268
|
if not clarification_ids:
|
|
@@ -350,6 +372,47 @@ def _preview_result(args) -> dict:
|
|
|
350
372
|
}
|
|
351
373
|
|
|
352
374
|
|
|
375
|
+
def _decision_for_run(args) -> IncrementalDecision:
|
|
376
|
+
"""SHA·폐포·컷오프 판정. unlinked 는 full 로 강등하지 않는다."""
|
|
377
|
+
data = load_owned_object(
|
|
378
|
+
Path(args.prev_data), artifact="incremental scope report"
|
|
379
|
+
)
|
|
380
|
+
stages = parse_stage_graph(data)
|
|
381
|
+
impacted = {
|
|
382
|
+
int(token.strip()) for token in args.impacted.split(",") if token.strip()
|
|
383
|
+
}
|
|
384
|
+
prep_ids = {
|
|
385
|
+
token.strip() for token in args.prep_items.split(",") if token.strip()
|
|
386
|
+
}
|
|
387
|
+
impacted.update(design_prep_impacted_stages(data, prep_ids))
|
|
388
|
+
answered = {
|
|
389
|
+
token.strip()
|
|
390
|
+
for token in args.answered_clarifications.split(",")
|
|
391
|
+
if token.strip()
|
|
392
|
+
}
|
|
393
|
+
linked, unlinked = partition_clarification_stages(data, answered)
|
|
394
|
+
if unlinked and not impacted:
|
|
395
|
+
return IncrementalDecision(
|
|
396
|
+
UNRESOLVED_MODE,
|
|
397
|
+
[],
|
|
398
|
+
[],
|
|
399
|
+
f"{', '.join(unlinked)} trace(s) to no stage in the prior report — "
|
|
400
|
+
"pass --impacted with the stage numbers those answers affect, or "
|
|
401
|
+
"--full-reason for a structural change",
|
|
402
|
+
)
|
|
403
|
+
impacted.update(linked)
|
|
404
|
+
unknown_stages = impacted - {stage for stage, _ in stages}
|
|
405
|
+
if unknown_stages:
|
|
406
|
+
unknown = ", ".join(str(stage) for stage in sorted(unknown_stages))
|
|
407
|
+
raise ValueError(f"impacted stage(s) absent from Stage Map: {unknown}")
|
|
408
|
+
return decide_scope(
|
|
409
|
+
stages=stages,
|
|
410
|
+
impacted_stages=impacted,
|
|
411
|
+
prev_base_sha=args.prev_base_sha,
|
|
412
|
+
cur_base_sha=args.cur_base_sha,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
353
416
|
def main(argv: list[str]) -> int:
|
|
354
417
|
ap = argparse.ArgumentParser(prog="okstra incremental-scope")
|
|
355
418
|
ap.add_argument("--prev-data", required=True, help="prior run final-report data.json")
|
|
@@ -395,31 +458,13 @@ def main(argv: list[str]) -> int:
|
|
|
395
458
|
return 0
|
|
396
459
|
|
|
397
460
|
try:
|
|
398
|
-
|
|
399
|
-
Path(args.prev_data), artifact="incremental scope report"
|
|
400
|
-
)
|
|
401
|
-
stages = parse_stage_graph(data)
|
|
402
|
-
impacted = {int(t.strip()) for t in args.impacted.split(",") if t.strip()}
|
|
403
|
-
prep_ids = {t.strip() for t in args.prep_items.split(",") if t.strip()}
|
|
404
|
-
impacted.update(design_prep_impacted_stages(data, prep_ids))
|
|
405
|
-
answered = {
|
|
406
|
-
t.strip() for t in args.answered_clarifications.split(",") if t.strip()
|
|
407
|
-
}
|
|
408
|
-
if answered:
|
|
409
|
-
impacted.update(clarification_impacted_stages(data, answered))
|
|
410
|
-
unknown_stages = impacted - {stage for stage, _ in stages}
|
|
411
|
-
if unknown_stages:
|
|
412
|
-
unknown = ", ".join(str(stage) for stage in sorted(unknown_stages))
|
|
413
|
-
raise ValueError(f"impacted stage(s) absent from Stage Map: {unknown}")
|
|
414
|
-
decision = decide_scope(
|
|
415
|
-
stages=stages, impacted_stages=impacted,
|
|
416
|
-
prev_base_sha=args.prev_base_sha, cur_base_sha=args.cur_base_sha,
|
|
417
|
-
)
|
|
461
|
+
decision = _decision_for_run(args)
|
|
418
462
|
# Every bad input degrades to a full re-verification rather than raising:
|
|
419
463
|
# `full` is always the safe answer, and the caller reads the decision off
|
|
420
464
|
# stdout, so a traceback would leave it with no decision at all. OSError
|
|
421
465
|
# covers an unreadable --prev-data, KeyError/TypeError a stageMap row that
|
|
422
|
-
# is missing `stage` or is not a mapping.
|
|
466
|
+
# is missing `stage` or is not a mapping. Unlinked ids are not this case
|
|
467
|
+
# — `_decision_for_run` returns `unresolved` instead of raising.
|
|
423
468
|
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
424
469
|
decision = IncrementalDecision(
|
|
425
470
|
"full", [], [], f"invalid incremental-scope input: {exc}",
|