simcon-toolkit 0.1.0__py3-none-any.whl
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.
- simcon_toolkit/__init__.py +40 -0
- simcon_toolkit/__main__.py +7 -0
- simcon_toolkit/_kit/LICENSE +202 -0
- simcon_toolkit/_kit/NOTICE +37 -0
- simcon_toolkit/_kit/assets/parts/clip_frame.stl +0 -0
- simcon_toolkit/_kit/assets/parts/simple_plate.stl +0 -0
- simcon_toolkit/_kit/packages/.ruff.toml +10 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/__init__.py +8 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/auth.py +681 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/client.py +235 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/__init__.py +5 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/mesh.py +210 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/stl.py +168 -0
- simcon_toolkit/_kit/packages/cadmould_results/__init__.py +30 -0
- simcon_toolkit/_kit/packages/cadmould_results/loader.py +288 -0
- simcon_toolkit/_kit/packages/cadmould_scoring/__init__.py +7 -0
- simcon_toolkit/_kit/packages/cadmould_scoring/metrics.py +519 -0
- simcon_toolkit/_kit/pyproject.toml +232 -0
- simcon_toolkit/_kit/templates/_shared/AGENTS.base.md +101 -0
- simcon_toolkit/_kit/templates/gate-study/.gitignore +18 -0
- simcon_toolkit/_kit/templates/gate-study/AGENTS.md +46 -0
- simcon_toolkit/_kit/templates/gate-study/GATING_STUDY_PLAYBOOK.md +219 -0
- simcon_toolkit/_kit/templates/gate-study/INITIAL_PROMPT.md +26 -0
- simcon_toolkit/_kit/templates/gate-study/README.md +137 -0
- simcon_toolkit/_kit/templates/gate-study/main.py +344 -0
- simcon_toolkit/_kit/templates/gate-study/pipeline.py +281 -0
- simcon_toolkit/_kit/templates/process-window/.gitignore +20 -0
- simcon_toolkit/_kit/templates/process-window/AGENTS.md +49 -0
- simcon_toolkit/_kit/templates/process-window/METHOD.md +155 -0
- simcon_toolkit/_kit/templates/process-window/README.md +176 -0
- simcon_toolkit/_kit/templates/process-window/configs/simple-plate.yaml +116 -0
- simcon_toolkit/_kit/templates/process-window/doe_spec.schema.md +249 -0
- simcon_toolkit/_kit/templates/process-window/main.py +82 -0
- simcon_toolkit/_kit/templates/process-window/process_window/__init__.py +5 -0
- simcon_toolkit/_kit/templates/process-window/process_window/centre.py +298 -0
- simcon_toolkit/_kit/templates/process-window/process_window/design.py +144 -0
- simcon_toolkit/_kit/templates/process-window/process_window/economics.py +367 -0
- simcon_toolkit/_kit/templates/process-window/process_window/emit.py +591 -0
- simcon_toolkit/_kit/templates/process-window/process_window/guardrails.py +153 -0
- simcon_toolkit/_kit/templates/process-window/process_window/harness.py +360 -0
- simcon_toolkit/_kit/templates/process-window/process_window/identity.py +92 -0
- simcon_toolkit/_kit/templates/process-window/process_window/inspect_part.py +184 -0
- simcon_toolkit/_kit/templates/process-window/process_window/kpis.py +355 -0
- simcon_toolkit/_kit/templates/process-window/process_window/material_card.py +163 -0
- simcon_toolkit/_kit/templates/process-window/process_window/probe_proxy.py +169 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_confirm.py +403 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_epsilon_floor.py +198 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_feedback.py +322 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_refine.py +279 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_screening.py +370 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_sweep.py +166 -0
- simcon_toolkit/_kit/templates/process-window/process_window/setup_campaign.py +312 -0
- simcon_toolkit/_kit/templates/process-window/process_window/surrogate.py +201 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_centre.py +169 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_design.py +113 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_guardrails.py +157 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_surrogate.py +127 -0
- simcon_toolkit/_kit/templates/process-window/process_window/units.py +152 -0
- simcon_toolkit/_kit/templates/quoting/.gitignore +24 -0
- simcon_toolkit/_kit/templates/quoting/AGENTS.md +58 -0
- simcon_toolkit/_kit/templates/quoting/INTERVIEW.md +147 -0
- simcon_toolkit/_kit/templates/quoting/METHOD.md +256 -0
- simcon_toolkit/_kit/templates/quoting/PROMPT.md +46 -0
- simcon_toolkit/_kit/templates/quoting/QUOTING_PLAYBOOK.md +245 -0
- simcon_toolkit/_kit/templates/quoting/README.md +158 -0
- simcon_toolkit/_kit/templates/quoting/main.py +484 -0
- simcon_toolkit/_kit/templates/quoting/parts/.gitkeep +0 -0
- simcon_toolkit/_kit/templates/quoting/quoting/__init__.py +11 -0
- simcon_toolkit/_kit/templates/quoting/quoting/costing.py +725 -0
- simcon_toolkit/_kit/templates/quoting/quoting/geometry.py +398 -0
- simcon_toolkit/_kit/templates/quoting/quoting/shop.py +193 -0
- simcon_toolkit/_kit/templates/quoting/quoting/state.py +260 -0
- simcon_toolkit/_kit/templates/quoting/quoting/study.py +577 -0
- simcon_toolkit/_kit/templates/quoting/quoting/toolkit.py +50 -0
- simcon_toolkit/_kit/templates/quoting/shop/README.md +43 -0
- simcon_toolkit/_kit/templates/quoting/shop/commercial.md +86 -0
- simcon_toolkit/_kit/templates/quoting/shop/lessons.md +94 -0
- simcon_toolkit/_kit/templates/quoting/shop/machines.md +68 -0
- simcon_toolkit/_kit/templates/quoting/shop/materials.md +92 -0
- simcon_toolkit/_kit/templates/quoting/shop/shop-profile.md +87 -0
- simcon_toolkit/_kit/templates/quoting/shop/tooling.md +145 -0
- simcon_toolkit/_kit/templates/run-one-simulation/.gitignore +16 -0
- simcon_toolkit/_kit/templates/run-one-simulation/AGENTS.md +41 -0
- simcon_toolkit/_kit/templates/run-one-simulation/README.md +133 -0
- simcon_toolkit/_kit/templates/run-one-simulation/main.py +216 -0
- simcon_toolkit/_kit/templates.toml +83 -0
- simcon_toolkit/choices.py +11 -0
- simcon_toolkit/cli.py +381 -0
- simcon_toolkit/generate.py +590 -0
- simcon_toolkit/instructions.py +152 -0
- simcon_toolkit/manifest.py +86 -0
- simcon_toolkit/project.py +356 -0
- simcon_toolkit/wizard.py +160 -0
- simcon_toolkit-0.1.0.dist-info/METADATA +48 -0
- simcon_toolkit-0.1.0.dist-info/RECORD +97 -0
- simcon_toolkit-0.1.0.dist-info/WHEEL +4 -0
- simcon_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Quote state, the local decision log, and the write-back that closes the learning loop.
|
|
2
|
+
|
|
3
|
+
A quote is identified by the *contents* of its part, never by the part's file name - see
|
|
4
|
+
:func:`part_key`. Its bookkeeping lives under ``cache/<key>/``; the deliverable it produces
|
|
5
|
+
lives under ``output/<key>/``.
|
|
6
|
+
|
|
7
|
+
Three artefacts per quote, all under ``cache/<key>/``:
|
|
8
|
+
|
|
9
|
+
* ``state.json`` - what has been decided so far, so a quote survives a lost session and the
|
|
10
|
+
part is meshed and uploaded exactly once.
|
|
11
|
+
* ``decisions.md`` - the local decision log: every fork, what was chosen, and why. The cloud
|
|
12
|
+
project notes are the shared copy of this; this one is the primary record.
|
|
13
|
+
* ``corrections.json``- the estimator's answers at review time, written by the agent from the
|
|
14
|
+
conversation and applied by :func:`apply_corrections`.
|
|
15
|
+
|
|
16
|
+
They sit in ``cache/`` rather than beside the quote because the next stage reads them: a
|
|
17
|
+
customer who clears ``output/`` must not lose the record that the following stage depends on.
|
|
18
|
+
|
|
19
|
+
:func:`apply_corrections` is the whole self-improving mechanism: it appends a prose lesson to
|
|
20
|
+
``shop/lessons.md`` **and** merges the numeric part into that file's ``calibration`` block, which
|
|
21
|
+
``costing.py`` reads on the next quote. Prose without a number teaches nobody; a number without
|
|
22
|
+
the prose is unmaintainable. Both, or neither.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
from datetime import date
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from .shop import CALIBRATION_DEFAULTS, SHOP_DIR, read_block, write_block
|
|
35
|
+
|
|
36
|
+
TEMPLATE_ROOT = Path(__file__).resolve().parents[1]
|
|
37
|
+
# Ours: the mesh, the cloud ids, and the record each stage hands the next. Deleting it
|
|
38
|
+
# costs a re-mesh and a re-upload, never a wrong answer.
|
|
39
|
+
CACHE_DIR = TEMPLATE_ROOT / "cache"
|
|
40
|
+
# Yours: the priced quote and the simulation results it was read from. Safe to delete.
|
|
41
|
+
OUTPUT_DIR = TEMPLATE_ROOT / "output"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NoSuchQuote(KeyError):
|
|
45
|
+
"""Raised when a quote is asked for by a key nothing has written."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def part_key(part: str | Path) -> str:
|
|
49
|
+
"""Quote identity for a part: its name, plus enough of its content to be honest.
|
|
50
|
+
|
|
51
|
+
The name alone is not an identity. Two customers can both send ``bracket.stl``, and
|
|
52
|
+
re-exporting a part changes its bytes but not its path - the ordinary working loop
|
|
53
|
+
here. Either way a name-keyed quote would price the new shape against the previous
|
|
54
|
+
revision's mesh, uploaded geometry and recorded decisions, with nothing to show it.
|
|
55
|
+
"""
|
|
56
|
+
part = Path(part)
|
|
57
|
+
with part.open("rb") as fh:
|
|
58
|
+
digest = hashlib.file_digest(fh, "sha256").hexdigest()[:12]
|
|
59
|
+
return f"{part.stem}-{digest}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AmbiguousQuote(RuntimeError):
|
|
63
|
+
"""Raised when a resuming stage was not told which quote it is resuming."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def part_for_stage(
|
|
67
|
+
explicit: str | Path | None,
|
|
68
|
+
default: str | Path,
|
|
69
|
+
*,
|
|
70
|
+
cache_root: str | Path = CACHE_DIR,
|
|
71
|
+
) -> Path:
|
|
72
|
+
"""The geometry a *resuming* stage acts on, refusing to guess between quotes.
|
|
73
|
+
|
|
74
|
+
A stage that continues a quote must not silently fall back to the bundled sample: a
|
|
75
|
+
customer who ran ``intake`` on their own part and then omitted the flag would have the
|
|
76
|
+
sample's quote re-studied and re-priced, and the deliverable written under the sample's
|
|
77
|
+
key - plausible output, wrong part, no warning. So the default stands only while it is
|
|
78
|
+
unambiguous, which is the demo case of the sample being the sole quote on file.
|
|
79
|
+
"""
|
|
80
|
+
if explicit is not None:
|
|
81
|
+
return Path(explicit)
|
|
82
|
+
default = Path(default)
|
|
83
|
+
known = quotes_on_file(cache_root)
|
|
84
|
+
default_key = part_key(default) if default.is_file() else None
|
|
85
|
+
if [k for k in known if k != default_key]:
|
|
86
|
+
listed = "\n ".join(known)
|
|
87
|
+
raise AmbiguousQuote(
|
|
88
|
+
"this stage resumes a quote, so it needs to know which one.\n"
|
|
89
|
+
"Pass --part pointing at the geometry you ran `intake` on.\n"
|
|
90
|
+
f"Quotes on file:\n {listed}"
|
|
91
|
+
)
|
|
92
|
+
return default
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def quotes_on_file(cache_root: str | Path = CACHE_DIR) -> list[str]:
|
|
96
|
+
"""Every quote that has actually been written, newest-looking last.
|
|
97
|
+
|
|
98
|
+
A directory alone does not count: a folder can exist before any stage has decided
|
|
99
|
+
anything, and offering that back as a resumable quote would be a lie.
|
|
100
|
+
"""
|
|
101
|
+
cache_root = Path(cache_root)
|
|
102
|
+
if not cache_root.is_dir():
|
|
103
|
+
return []
|
|
104
|
+
return sorted(p.name for p in cache_root.iterdir() if (p / "state.json").is_file())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ==========================================================================
|
|
108
|
+
# per-quote state
|
|
109
|
+
# ==========================================================================
|
|
110
|
+
class QuoteState:
|
|
111
|
+
"""A small json file that makes a quote resumable."""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
key: str,
|
|
116
|
+
*,
|
|
117
|
+
cache_root: str | Path = CACHE_DIR,
|
|
118
|
+
output_root: str | Path = OUTPUT_DIR,
|
|
119
|
+
):
|
|
120
|
+
self.key = key
|
|
121
|
+
self.dir = Path(cache_root) / key
|
|
122
|
+
self.output_dir = Path(output_root) / key
|
|
123
|
+
# Deliberately no mkdir here: opening a quote must leave no trace, or every
|
|
124
|
+
# mistyped key litters the cache with an empty folder that nothing cleans up.
|
|
125
|
+
self.path = self.dir / "state.json"
|
|
126
|
+
self.data: dict[str, Any] = json.loads(self.path.read_text(encoding="utf-8")) if self.path.exists() else {}
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def for_part(cls, part: str | Path, **roots: str | Path) -> QuoteState:
|
|
130
|
+
"""The quote for this geometry, opened or started. Keyed on the file's contents."""
|
|
131
|
+
return cls(part_key(part), **roots)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def open_existing(cls, key: str, **roots: str | Path) -> QuoteState:
|
|
135
|
+
"""An existing quote, by key. Refuses to invent one, and says what it does have.
|
|
136
|
+
|
|
137
|
+
Starting a fresh quote here would be silent and wrong: the stage that needs this
|
|
138
|
+
writes the estimator's corrections, so an empty record swallows the lesson.
|
|
139
|
+
"""
|
|
140
|
+
quote = cls(key, **roots)
|
|
141
|
+
if not quote.path.exists():
|
|
142
|
+
known = quotes_on_file(quote.dir.parent)
|
|
143
|
+
listed = "\n ".join(known) if known else "(none yet - run `intake` first)"
|
|
144
|
+
raise NoSuchQuote(f"no quote on file for '{key}'. Quotes that do exist:\n {listed}")
|
|
145
|
+
return quote
|
|
146
|
+
|
|
147
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
148
|
+
return self.data.get(key, default)
|
|
149
|
+
|
|
150
|
+
def update(self, **kw: Any) -> None:
|
|
151
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
self.data.update(kw)
|
|
153
|
+
self.path.write_text(json.dumps(self.data, indent=2, default=str), encoding="utf-8")
|
|
154
|
+
|
|
155
|
+
# -- the local decision log -------------------------------------------
|
|
156
|
+
@property
|
|
157
|
+
def decisions_path(self) -> Path:
|
|
158
|
+
return self.dir / "decisions.md"
|
|
159
|
+
|
|
160
|
+
def decide(self, title: str, choice: str, why: str, *, alternatives: str = "") -> None:
|
|
161
|
+
"""Append one decision. A fork with no recorded reason is a fork you will re-argue."""
|
|
162
|
+
path = self.decisions_path
|
|
163
|
+
if not path.exists():
|
|
164
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
path.write_text(
|
|
166
|
+
f"# Decision log - {self.dir.name}\n\nEvery fork in this quote, what was chosen, "
|
|
167
|
+
"and why. Written as the quote runs, not summarised afterwards.\n",
|
|
168
|
+
encoding="utf-8",
|
|
169
|
+
)
|
|
170
|
+
block = [f"\n## {date.today().isoformat()} - {title}\n", f"**Chosen:** {choice}\n", f"**Why:** {why}\n"]
|
|
171
|
+
if alternatives:
|
|
172
|
+
block.append(f"**Alternatives considered:** {alternatives}\n")
|
|
173
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
174
|
+
fh.write("".join(block))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ==========================================================================
|
|
178
|
+
# the learning loop
|
|
179
|
+
# ==========================================================================
|
|
180
|
+
CORRECTIONS_TEMPLATE: dict[str, Any] = {
|
|
181
|
+
"quote": "<part name>",
|
|
182
|
+
"reviewer": "<who corrected it>",
|
|
183
|
+
"corrections": [
|
|
184
|
+
{
|
|
185
|
+
"title": "<one line, e.g. 'Cycle time too optimistic'>",
|
|
186
|
+
"agent_decided": "<what the agent put in the quote>",
|
|
187
|
+
"corrected_to": "<what it should have been>",
|
|
188
|
+
"why": "<the estimator's own words - this is the part that transfers>",
|
|
189
|
+
"rule": "<what to do differently next time>",
|
|
190
|
+
"calibration": {"<key>": 0.0},
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def next_lesson_id(lessons_md: Path) -> str:
|
|
197
|
+
ids = [int(m) for m in re.findall(r"^## L-(\d{3})", lessons_md.read_text(encoding="utf-8"), re.M)]
|
|
198
|
+
return f"L-{max(ids) + 1 if ids else 1:03d}"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def apply_corrections(corrections: dict[str, Any], *, shop_dir: Path = SHOP_DIR, today: str | None = None) -> list[str]:
|
|
202
|
+
"""Write the estimator's corrections into ``shop/lessons.md``. Returns the lesson ids added.
|
|
203
|
+
|
|
204
|
+
Numeric changes are validated against the known calibration knobs before anything is written -
|
|
205
|
+
a typo'd key would otherwise sit in the file looking effective while changing nothing.
|
|
206
|
+
"""
|
|
207
|
+
lessons = Path(shop_dir) / "lessons.md"
|
|
208
|
+
stamp = today or date.today().isoformat()
|
|
209
|
+
quote_name = corrections.get("quote", "unnamed quote")
|
|
210
|
+
items = corrections.get("corrections", [])
|
|
211
|
+
if not items:
|
|
212
|
+
raise ValueError("no corrections given - nothing to learn")
|
|
213
|
+
|
|
214
|
+
calibration = read_block(lessons, "calibration")
|
|
215
|
+
text = lessons.read_text(encoding="utf-8")
|
|
216
|
+
marker = "<!-- New entries are appended here by `main.py learn`. -->"
|
|
217
|
+
if marker not in text:
|
|
218
|
+
raise ValueError("shop/lessons.md has lost its append marker - restore it before learning")
|
|
219
|
+
|
|
220
|
+
first_id = int(next_lesson_id(lessons)[2:])
|
|
221
|
+
added: list[str] = []
|
|
222
|
+
entries: list[str] = []
|
|
223
|
+
for n, item in enumerate(items):
|
|
224
|
+
for key in item.get("calibration") or {}:
|
|
225
|
+
if key not in CALIBRATION_DEFAULTS:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"'{key}' is not a calibration knob the cost model reads. "
|
|
228
|
+
f"Known: {', '.join(sorted(CALIBRATION_DEFAULTS))}"
|
|
229
|
+
)
|
|
230
|
+
lesson_id = f"L-{first_id + n:03d}"
|
|
231
|
+
added.append(lesson_id)
|
|
232
|
+
changed = []
|
|
233
|
+
for key, value in (item.get("calibration") or {}).items():
|
|
234
|
+
old = calibration.get(key, CALIBRATION_DEFAULTS[key])
|
|
235
|
+
calibration[key] = value
|
|
236
|
+
changed.append(f"`calibration.{key}` {old} -> {value}")
|
|
237
|
+
entries.append(
|
|
238
|
+
"\n".join(
|
|
239
|
+
[
|
|
240
|
+
f"## {lesson_id} - {stamp} - {quote_name} - {item.get('title', 'correction')}",
|
|
241
|
+
"",
|
|
242
|
+
f"**Agent decided:** {item.get('agent_decided', '-')}",
|
|
243
|
+
"",
|
|
244
|
+
f"**Corrected to:** {item.get('corrected_to', '-')}",
|
|
245
|
+
"",
|
|
246
|
+
f"**Why (estimator's words):** {item.get('why', '-')}",
|
|
247
|
+
"",
|
|
248
|
+
f"**Rule for next time:** {item.get('rule', '-')}",
|
|
249
|
+
"",
|
|
250
|
+
"**Changed:** " + ("; ".join(changed) if changed else "nothing numeric - judgement only"),
|
|
251
|
+
"",
|
|
252
|
+
"---",
|
|
253
|
+
"",
|
|
254
|
+
]
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
lessons.write_text(text.replace(marker, "\n".join(entries) + "\n" + marker), encoding="utf-8")
|
|
259
|
+
write_block(lessons, "calibration", calibration)
|
|
260
|
+
return added
|