File size: 5,039 Bytes
9c84f9d
 
 
 
 
dd41633
9c84f9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""์„ค์ •์˜ **๋‚ด์šฉ ์ง€๋ฌธ** โ€” ์ด๋ฆ„์ด ์•„๋‹ˆ๋ผ ๋‚ด์šฉ์œผ๋กœ ์‹คํ—˜์„ ์‹๋ณ„ํ•œ๋‹ค.

์™œ ํ•„์š”ํ•œ๊ฐ€: ์ด ์—”์ง„์˜ ์ž์‚ฐ์€ ์˜ค๋ž˜ ์ด๋ฆ„์œผ๋กœ๋งŒ ์‹๋ณ„๋ผ ์™”๊ณ (`prompt_dev_v23`
โ†” `pipeline_dev_v24` ์ฒ˜๋Ÿผ ์ง์˜ ๋ฒˆํ˜ธ๊ฐ€ ์–ด๊ธ‹๋‚˜๋Š” ์ผ์ด ์žฆ์•˜๋‹ค), ๊ทธ๋ž˜์„œ
"์ด๋ฆ„์ด ๋‹ค๋ฅธ ๋‘ run ์ด ์‚ฌ์‹ค ๊ฐ™์€ ์„ค์ •" ์ธ์ง€๋ฅผ ์‚ฌ๋žŒ์ด ํ•ด์‹œ๋ฅผ ๋– ์„œ ๋Œ€์กฐํ•ด์•ผ ํ–ˆ๋‹ค
(`insights/lab_notes_archive.md#260803_etd_regression_prod_vs_v24` ์˜ '์ด๋ฆ„ ๋งคํ•‘' ์ ˆ์ด ๊ทธ ํ”์ ์ด๋‹ค).
run ์— ์ง€๋ฌธ์„ ์ฐ์–ด ๋‘๋ฉด ๊ทธ ๋Œ€์กฐ๊ฐ€ ์กฐํšŒ ํ•œ ๋ฒˆ์œผ๋กœ ๋๋‚œ๋‹ค.

์ง€๋ฌธ์€ **ํ•ด์„์ด ๋๋‚œ ์„ค์ •**์„ ๋Œ€์ƒ์œผ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค. `extends`/override ๋กœ ์กฐ๋ฆฝํ•œ
ํŒŒ์ดํ”„๋ผ์ธ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์„ ํ†ต์งธ๋กœ ์ ์–ด ๋‘” ํŒŒ์ผ์€ ๊ฐ™์€ ์ง€๋ฌธ์„ ๊ฐ€์ ธ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค
(๊ทธ ๋“ฑ๊ฐ€์„ฑ์ด ํŒŒ์ผ ๋ณต์ œ๋ฅผ override ๋กœ ์˜ฎ๊ธธ ๋•Œ์˜ ์•ˆ์ „๋ง์ด๋‹ค).

ํ˜•์‹: `sha256:<์•ž 12์ž>` โ€” ๋ˆˆ์œผ๋กœ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์„ ๋งŒํผ ์งง๊ณ , ํ”„๋กœ์ ํŠธ๋‹น ์ˆ˜๋ฐฑ ๊ฐœ
๊ทœ๋ชจ์—์„œ ์ถฉ๋Œํ•˜์ง€ ์•Š์„ ๋งŒํผ ๊ธธ๋‹ค.
"""

import hashlib
import json
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any

#: ์ง€๋ฌธ ๊ณ„์‚ฐ์—์„œ ์ œ์™ธํ•˜๋Š” ํ‚ค โ€” ์‚ฌ๋žŒ์ด ์ฝ๋Š” ๋ผ๋ฒจ์ผ ๋ฟ ์‹คํ–‰์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค.
#: ์ด๊ฑธ ์ œ์™ธํ•ด์•ผ "์ด๋ฆ„๋งŒ ๋ฐ”๊พผ ์‚ฌ๋ณธ"์ด ๊ฐ™์€ ์ง€๋ฌธ์œผ๋กœ ์žกํžŒ๋‹ค.
NON_FUNCTIONAL_KEYS = frozenset({"name", "description", "comment", "extends", "overrides"})

#: ์Šคํ… ํ•˜์œ„์—์„œ ์ œ์™ธํ•˜๋Š” ํ‚ค โ€” ์Šคํ…์˜ `name` ์€ ํ”„๋กฌํ”„ํŠธ ํ‚คยทrule ํƒ€์ž… ์กฐํšŒ์—
#: ์“ฐ์ด๋ฏ€๋กœ (`pipelines/registry.py:resolve_step`) ๊ธฐ๋Šฅ์ ์ด๋‹ค. ์ง€์šฐ๋ฉด ์•ˆ ๋œ๋‹ค.
NON_FUNCTIONAL_STEP_KEYS = frozenset({"description", "comment"})

#: ํ”„๋กฌํ”„ํŠธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ์ง€๋ฌธ ๋Œ€์ƒ์œผ๋กœ ์‚ผ๋Š” ํ™•์žฅ์ž.
PROMPT_ASSET_SUFFIXES = frozenset({".txt", ".yaml", ".yml", ".json"})

_DIGEST_PREFIX = "sha256"
_DIGEST_LENGTH = 12


def digest(payload: Any) -> str:
    """JSON ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ๊ฐ’์˜ ์ •๊ทœ ์ง€๋ฌธ."""
    canonical = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
    full = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
    return f"{_DIGEST_PREFIX}:{full[:_DIGEST_LENGTH]}"


def pipeline_fingerprint(config: Mapping[str, Any] | None) -> str | None:
    """ํ•ด์„์ด ๋๋‚œ ํŒŒ์ดํ”„๋ผ์ธ ์„ค์ •์˜ ์ง€๋ฌธ.

    Args:
        config: `extends`/override ํ•ด์„๊นŒ์ง€ ๋๋‚œ ํŒŒ์ดํ”„๋ผ์ธ dict.
            None ์ด๊ฑฐ๋‚˜ ๋น„์–ด ์žˆ์œผ๋ฉด None ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค (single_step ์ฒ˜๋Ÿผ ํŒŒ์ดํ”„๋ผ์ธ
            ํŒŒ์ผ์ด ์—†๋Š” ํƒœ์Šคํฌ๊ฐ€ ์žˆ๋‹ค).

    Returns:
        `sha256:xxxxxxxxxxxx` ๋˜๋Š” None.
    """
    if not config:
        return None
    return digest(_strip_pipeline(config))


def prompt_fingerprint(path: Path) -> str | None:
    """ํ”„๋กฌํ”„ํŠธ ์ž์‚ฐ์˜ ์ง€๋ฌธ โ€” ๋””๋ ‰ํ† ๋ฆฌ(์—ฌ๋Ÿฌ ํŒŒ์ผ)์™€ ๋‹จ์ผ ํŒŒ์ผ ๋ชจ๋‘ ์ง€์›ํ•œ๋‹ค.

    ๋””๋ ‰ํ† ๋ฆฌ๋ฉด ํ•˜์œ„ ์ž์‚ฐ ํŒŒ์ผ์„ ์ƒ๋Œ€๊ฒฝ๋กœ๋กœ ์ •๋ ฌํ•ด `{๊ฒฝ๋กœ: ๋‚ด์šฉํ•ด์‹œ}` ๋งต์„ ๋งŒ๋“ค๊ณ 
    ๊ทธ ๋งต์˜ ์ง€๋ฌธ์„ ๋‚ธ๋‹ค. ํŒŒ์ผ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ „์ฒด ์ง€๋ฌธ์ด ๋ฐ”๋€Œ๊ณ , ํŒŒ์ผ ์ˆœ์„œ๋‚˜
    ๋””๋ ‰ํ† ๋ฆฌ ์ด๋ฆ„์ด ๋ฐ”๋€Œ์–ด๋„ ๋‚ด์šฉ์ด ๊ฐ™์œผ๋ฉด ์ง€๋ฌธ์€ ์œ ์ง€๋œ๋‹ค.

    Args:
        path: ํ”„๋กฌํ”„ํŠธ ๋””๋ ‰ํ† ๋ฆฌ ๋˜๋Š” YAML ํŒŒ์ผ ๊ฒฝ๋กœ.

    Returns:
        `sha256:xxxxxxxxxxxx`, ๊ฒฝ๋กœ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ฝ์„ ์ˆ˜ ์—†์œผ๋ฉด None.
    """
    try:
        if path.is_dir():
            assets = _collect_prompt_assets(path)
            if not assets:
                return None
            return digest(assets)
        if path.is_file():
            return digest({path.name: _file_digest(path)})
    except OSError:
        return None
    return None


def _collect_prompt_assets(root: Path) -> dict[str, str]:
    """ํ”„๋กฌํ”„ํŠธ ๋””๋ ‰ํ† ๋ฆฌ์˜ `{์ƒ๋Œ€๊ฒฝ๋กœ: ํŒŒ์ผ์ง€๋ฌธ}` ๋งต. ์ˆจ๊น€ ํŒŒ์ผ์€ ์ œ์™ธํ•œ๋‹ค."""
    assets: dict[str, str] = {}
    for file in sorted(root.rglob("*")):
        if not file.is_file() or file.suffix.lower() not in PROMPT_ASSET_SUFFIXES:
            continue
        if any(part.startswith(".") for part in file.relative_to(root).parts):
            continue
        assets[file.relative_to(root).as_posix()] = _file_digest(file)
    return assets


def _file_digest(path: Path) -> str:
    """ํŒŒ์ผ ๋ฐ”์ดํŠธ์˜ ์ง€๋ฌธ โ€” ํ…์ŠคํŠธ ์ธ์ฝ”๋”ฉยท๊ฐœํ–‰ ์ •๊ทœํ™”๋ฅผ ํ•˜์ง€ ์•Š๋Š”๋‹ค(์žˆ๋Š” ๊ทธ๋Œ€๋กœ)."""
    return hashlib.sha256(path.read_bytes()).hexdigest()[:_DIGEST_LENGTH]


def _strip_pipeline(config: Mapping[str, Any]) -> dict[str, Any]:
    """ํŒŒ์ดํ”„๋ผ์ธ dict ์—์„œ ๋น„๊ธฐ๋Šฅ ํ•„๋“œ๋ฅผ ๊ฑท์–ด๋‚ธ ์‚ฌ๋ณธ."""
    stripped = {k: v for k, v in config.items() if k not in NON_FUNCTIONAL_KEYS}
    steps = stripped.get("steps")
    if isinstance(steps, Sequence) and not isinstance(steps, str | bytes):
        stripped["steps"] = [_strip_step(s) for s in steps]
    return stripped


def _strip_step(step: Any) -> Any:
    if not isinstance(step, Mapping):
        return step
    return {k: v for k, v in step.items() if k not in NON_FUNCTIONAL_STEP_KEYS}