agent-bios 0.4.0 → 0.7.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/claude/CLAUDE.md +4 -0
- package/claude/guides/learning-flow.md +106 -0
- package/codex/AGENTS.md +4 -0
- package/codex/guides/learning-flow.md +106 -0
- package/config/domains.json +150 -0
- package/config/learning.schema.json +104 -0
- package/config/promotions.json +4 -0
- package/package.json +8 -1
- package/scripts/check-learning.py +231 -0
- package/scripts/check-parity.sh +36 -0
- package/scripts/collect-learning.py +600 -0
- package/scripts/install.sh +19 -0
- package/scripts/migrate-learnings.py +423 -0
- package/scripts/redact.py +91 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Learning record gate + validator (collection loop, Phase 0).
|
|
3
|
+
|
|
4
|
+
config/learning.schema.json is the SSOT for the learning record
|
|
5
|
+
(design/collection-loop/DESIGN.md); the dashboard mirrors only minimal
|
|
6
|
+
validation. Full client-side validity = JSON Schema conformance PLUS domain
|
|
7
|
+
membership in config/domains.json domains ∪ 'unclassified' — membership is
|
|
8
|
+
checked here, not frozen in the schema, so vocabulary evolution never needs
|
|
9
|
+
a schema_version bump.
|
|
10
|
+
|
|
11
|
+
Modes:
|
|
12
|
+
(no args) gate: schema validates against its 2020-12 metaschema;
|
|
13
|
+
every fixtures/valid-*.json passes; every fixtures/broken-*.json
|
|
14
|
+
fails AND the failure names the field the fixture breaks
|
|
15
|
+
(a broken fixture failing for an unrelated reason is a FAIL);
|
|
16
|
+
non-vacuity: >=1 valid fixture, >=1 broken fixture,
|
|
17
|
+
>=1 registered domain.
|
|
18
|
+
<file.json>… validate the given record file(s); exit 0 iff all valid.
|
|
19
|
+
This is the client-side validation entry point for Phase 1.
|
|
20
|
+
--self-test negative controls: in-memory mutations of a valid record
|
|
21
|
+
(each required field dropped, plus type/pattern/membership
|
|
22
|
+
mutations) must ALL be rejected; the unmutated record must
|
|
23
|
+
pass. Proves the gate can fail.
|
|
24
|
+
|
|
25
|
+
Any violation exits 1 with all violations listed.
|
|
26
|
+
"""
|
|
27
|
+
import json
|
|
28
|
+
import pathlib
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import jsonschema
|
|
33
|
+
except ImportError:
|
|
34
|
+
sys.exit("check-learning: the 'jsonschema' package is required "
|
|
35
|
+
"(pip install jsonschema; verified 4.26.0 in DEPENDENCIES.md)")
|
|
36
|
+
|
|
37
|
+
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
38
|
+
SCHEMA = REPO / "config" / "learning.schema.json"
|
|
39
|
+
DOMAINS = REPO / "config" / "domains.json"
|
|
40
|
+
FIXTURES = REPO / "design" / "collection-loop" / "fixtures"
|
|
41
|
+
|
|
42
|
+
# broken fixture -> field its single defect lives in; the reported errors
|
|
43
|
+
# must mention it, so a fixture failing for an accidental other reason fails
|
|
44
|
+
# the gate instead of silently passing as "broken as expected".
|
|
45
|
+
BROKEN_EXPECT = {
|
|
46
|
+
"broken-missing-lesson.json": "lesson",
|
|
47
|
+
"broken-unknown-domain.json": "domain",
|
|
48
|
+
"broken-schema-version.json": "schema_version",
|
|
49
|
+
"broken-extra-field.json": "email",
|
|
50
|
+
"broken-empty-sessions.json": "supporting_sessions",
|
|
51
|
+
"broken-bad-created.json": "created",
|
|
52
|
+
"broken-proposed-domain-conflict.json": "domain",
|
|
53
|
+
"broken-bad-classification.json": "classification",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_json(path):
|
|
58
|
+
with open(path, encoding="utf-8") as f:
|
|
59
|
+
return json.load(f)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def valid_domain_values(path=DOMAINS):
|
|
63
|
+
"""The set a record's `domain` may take, drawn from config/domains.json.
|
|
64
|
+
|
|
65
|
+
Ledger-compatible (design/session-distill/ledger.json): the ledger's
|
|
66
|
+
`domain` field uses BOTH domain keys (builder-base, …) for
|
|
67
|
+
domain-specific lessons AND tier names (core, infra, …) for cross-cutting
|
|
68
|
+
ones — 4 real entries carry core/infra. So the valid set is the union of
|
|
69
|
+
both registered vocabularies plus 'unclassified' (refinement B — the
|
|
70
|
+
not-yet-triaged escape; kept distinct from 'core', which means a genuinely
|
|
71
|
+
cross-cutting lesson). Reuses existing vocabulary; introduces none.
|
|
72
|
+
"""
|
|
73
|
+
manifest = load_json(path)
|
|
74
|
+
values = set(manifest["domains"]) | set(manifest["tiers"]) | {"unclassified"}
|
|
75
|
+
if len(values) <= 1:
|
|
76
|
+
sys.exit("FAIL: config/domains.json registers no domains/tiers (vacuous gate)")
|
|
77
|
+
return values
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def validate_record(record, validator, domain_values):
|
|
81
|
+
"""Full validity: schema conformance + domain membership. Returns error strings."""
|
|
82
|
+
errors = []
|
|
83
|
+
for e in validator.iter_errors(record):
|
|
84
|
+
where = "/".join(str(p) for p in e.path) or "<root>"
|
|
85
|
+
errors.append(f"{where}: {e.message}")
|
|
86
|
+
if isinstance(record, dict):
|
|
87
|
+
dom = record.get("domain")
|
|
88
|
+
if isinstance(dom, str) and dom not in domain_values:
|
|
89
|
+
errors.append(
|
|
90
|
+
f"domain: {dom!r} is not a registered domain key, tier name, "
|
|
91
|
+
f"or 'unclassified' (config/domains.json)")
|
|
92
|
+
return errors
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_validator():
|
|
96
|
+
schema = load_json(SCHEMA)
|
|
97
|
+
jsonschema.Draft202012Validator.check_schema(schema)
|
|
98
|
+
return jsonschema.Draft202012Validator(schema)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def gate():
|
|
102
|
+
validator = build_validator()
|
|
103
|
+
domain_values = valid_domain_values()
|
|
104
|
+
failures = []
|
|
105
|
+
|
|
106
|
+
valid_fx = sorted(FIXTURES.glob("valid-*.json"))
|
|
107
|
+
broken_fx = sorted(FIXTURES.glob("broken-*.json"))
|
|
108
|
+
if not valid_fx:
|
|
109
|
+
failures.append(f"no valid-*.json fixtures in {FIXTURES} (vacuous)")
|
|
110
|
+
if not broken_fx:
|
|
111
|
+
failures.append(f"no broken-*.json fixtures in {FIXTURES} (vacuous)")
|
|
112
|
+
|
|
113
|
+
for fx in valid_fx:
|
|
114
|
+
errors = validate_record(load_json(fx), validator, domain_values)
|
|
115
|
+
if errors:
|
|
116
|
+
failures.append(f"{fx.name} must validate but failed: {'; '.join(errors)}")
|
|
117
|
+
|
|
118
|
+
for fx in broken_fx:
|
|
119
|
+
errors = validate_record(load_json(fx), validator, domain_values)
|
|
120
|
+
expect = BROKEN_EXPECT.get(fx.name)
|
|
121
|
+
if expect is None:
|
|
122
|
+
failures.append(f"{fx.name} has no BROKEN_EXPECT entry (unmapped negative control)")
|
|
123
|
+
elif not errors:
|
|
124
|
+
failures.append(f"{fx.name} must FAIL but validated (negative control broken)")
|
|
125
|
+
elif not any(expect in err for err in errors):
|
|
126
|
+
failures.append(
|
|
127
|
+
f"{fx.name} failed for the wrong reason (expected mention of "
|
|
128
|
+
f"{expect!r}): {'; '.join(errors)}")
|
|
129
|
+
for name in BROKEN_EXPECT:
|
|
130
|
+
if not (FIXTURES / name).is_file():
|
|
131
|
+
failures.append(f"BROKEN_EXPECT names a missing fixture: {name}")
|
|
132
|
+
|
|
133
|
+
if failures:
|
|
134
|
+
print("check-learning: FAIL")
|
|
135
|
+
for f in failures:
|
|
136
|
+
print(f" - {f}")
|
|
137
|
+
return 1
|
|
138
|
+
print(f"check-learning: OK ({len(valid_fx)} valid, "
|
|
139
|
+
f"{len(broken_fx)} negative controls)")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def self_test():
|
|
144
|
+
validator = build_validator()
|
|
145
|
+
domain_values = valid_domain_values()
|
|
146
|
+
base = load_json(FIXTURES / "valid-minimal.json")
|
|
147
|
+
schema = load_json(SCHEMA)
|
|
148
|
+
|
|
149
|
+
def drop(field):
|
|
150
|
+
r = dict(base)
|
|
151
|
+
del r[field]
|
|
152
|
+
return r
|
|
153
|
+
|
|
154
|
+
def swap(field, value):
|
|
155
|
+
return {**base, field: value}
|
|
156
|
+
|
|
157
|
+
mutations = [(f"drop required '{f}'", drop(f), f) for f in schema["required"]]
|
|
158
|
+
mutations += [
|
|
159
|
+
("schema_version as string", swap("schema_version", "1"), "schema_version"),
|
|
160
|
+
("uppercase learning_id", swap("learning_id", base["learning_id"].upper()), "learning_id"),
|
|
161
|
+
("lesson below minLength", swap("lesson", "short"), "lesson"),
|
|
162
|
+
("capitalized domain", swap("domain", "Builder-Base"), "domain"),
|
|
163
|
+
("session id without tool prefix", swap("supporting_sessions", ["517fbcea"]), "supporting_sessions"),
|
|
164
|
+
("created without T/zone", swap("created", "20260720T050000Z"), "created"),
|
|
165
|
+
("unknown extra key", {**base, "user_email": "x@y"}, "user_email"),
|
|
166
|
+
("classification type out of A-G enum", swap("classification", {"type": "Z"}), "classification"),
|
|
167
|
+
("classification unknown sub-key", swap("classification", {"mechanism": "x"}), "classification"),
|
|
168
|
+
("proposed_domain with a concrete domain", {**base, "proposed_domain": "data-pipeline", "domain": "builder-base"}, "domain"),
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
failures = []
|
|
172
|
+
if validate_record(base, validator, domain_values):
|
|
173
|
+
failures.append("positive control (valid-minimal) does not validate")
|
|
174
|
+
# positive control: a tier-name domain (real ledger precedent) must pass.
|
|
175
|
+
if validate_record({**base, "domain": "core"}, validator, domain_values):
|
|
176
|
+
failures.append("positive control (domain='core' tier name) does not validate")
|
|
177
|
+
# positive control: the optional slim classification block must pass.
|
|
178
|
+
if validate_record({**base, "classification": {"type": "B", "layer": "hook", "meets_bar": True}}, validator, domain_values):
|
|
179
|
+
failures.append("positive control (valid classification block) does not validate")
|
|
180
|
+
# positive control: proposed_domain rides only with an unclassified domain (base is unclassified).
|
|
181
|
+
if validate_record({**base, "proposed_domain": "data-pipeline"}, validator, domain_values):
|
|
182
|
+
failures.append("positive control (proposed_domain + unclassified) does not validate")
|
|
183
|
+
for name, record, expect in mutations:
|
|
184
|
+
errors = validate_record(record, validator, domain_values)
|
|
185
|
+
if not errors:
|
|
186
|
+
failures.append(f"mutation NOT caught: {name}")
|
|
187
|
+
elif not any(expect in err for err in errors):
|
|
188
|
+
failures.append(f"mutation caught for the wrong reason: {name} -> {errors}")
|
|
189
|
+
|
|
190
|
+
if failures:
|
|
191
|
+
print("check-learning --self-test: FAIL")
|
|
192
|
+
for f in failures:
|
|
193
|
+
print(f" - {f}")
|
|
194
|
+
return 1
|
|
195
|
+
print(f"check-learning --self-test: OK ({len(mutations)} mutations caught, "
|
|
196
|
+
f"4 positive controls)")
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def validate_files(paths):
|
|
201
|
+
validator = build_validator()
|
|
202
|
+
domain_values = valid_domain_values()
|
|
203
|
+
rc = 0
|
|
204
|
+
for p in paths:
|
|
205
|
+
# Client entry point (Phase 1): inputs are not hand-authored fixtures,
|
|
206
|
+
# so malformed JSON / unreadable files report cleanly, never a traceback.
|
|
207
|
+
try:
|
|
208
|
+
record = load_json(p)
|
|
209
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
210
|
+
rc = 1
|
|
211
|
+
print(f"INVALID {p}")
|
|
212
|
+
print(f" - not readable JSON: {e}")
|
|
213
|
+
continue
|
|
214
|
+
errors = validate_record(record, validator, domain_values)
|
|
215
|
+
if errors:
|
|
216
|
+
rc = 1
|
|
217
|
+
print(f"INVALID {p}")
|
|
218
|
+
for e in errors:
|
|
219
|
+
print(f" - {e}")
|
|
220
|
+
else:
|
|
221
|
+
print(f"OK {p}")
|
|
222
|
+
return rc
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
args = sys.argv[1:]
|
|
227
|
+
if args == ["--self-test"]:
|
|
228
|
+
sys.exit(self_test())
|
|
229
|
+
if args:
|
|
230
|
+
sys.exit(validate_files(args))
|
|
231
|
+
sys.exit(gate())
|
package/scripts/check-parity.sh
CHANGED
|
@@ -135,6 +135,42 @@ if [ -f config/learning.schema.json ]; then
|
|
|
135
135
|
|| { echo "FAIL: learning record gate (run scripts/check-learning.py)"; fail=1; }
|
|
136
136
|
python3 scripts/check-learning.py --self-test >/dev/null \
|
|
137
137
|
|| { echo "FAIL: learning gate self-test missed a negative control"; fail=1; }
|
|
138
|
+
# collect-learning Phase 2 transport: the watermark upload-drain logic
|
|
139
|
+
# (status classification, watermark advance, transient-stop, poison-skip)
|
|
140
|
+
# plus the capture-time secret-redaction wiring.
|
|
141
|
+
python3 scripts/collect-learning.py --self-test >/dev/null \
|
|
142
|
+
|| { echo "FAIL: collect-learning upload-drain self-test"; fail=1; }
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Secret-redaction floor (scripts/redact.py) — single-sourced by the heavy
|
|
146
|
+
# (digest.py) and light (collect-learning.py) flows; --self-test proves each
|
|
147
|
+
# pattern fires and that lessons ABOUT secrets are not over-redacted.
|
|
148
|
+
if [ -f scripts/redact.py ]; then
|
|
149
|
+
python3 scripts/redact.py --self-test >/dev/null \
|
|
150
|
+
|| { echo "FAIL: secret-redaction floor self-test (scripts/redact.py)"; fail=1; }
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# Phase 3 curation intake (scripts/ingest-learnings-export.py) — validates a
|
|
154
|
+
# dashboard learnings export and maps it to ledger candidates; --self-test
|
|
155
|
+
# proves valid rows map (cardinality > 0) and broken/non-v1 rows are diverted.
|
|
156
|
+
if [ -f scripts/ingest-learnings-export.py ]; then
|
|
157
|
+
python3 scripts/ingest-learnings-export.py --self-test >/dev/null \
|
|
158
|
+
|| { echo "FAIL: curation-intake self-test (scripts/ingest-learnings-export.py)"; fail=1; }
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# Phase 4 promote->migrate: the promotion manifest (config/promotions.json) is
|
|
162
|
+
# DERIVED from the ledger — --check fails if it is stale, so a promotion can't
|
|
163
|
+
# ship without its manifest entry; migrate-learnings clears personal copies only
|
|
164
|
+
# for in-bundle promotions (--self-test proves the not-in-bundle keep guard).
|
|
165
|
+
if [ -f scripts/build-promotions.py ]; then
|
|
166
|
+
python3 scripts/build-promotions.py --self-test >/dev/null \
|
|
167
|
+
|| { echo "FAIL: build-promotions self-test"; fail=1; }
|
|
168
|
+
python3 scripts/build-promotions.py --check >/dev/null \
|
|
169
|
+
|| { echo "FAIL: config/promotions.json is stale vs the ledger (run scripts/build-promotions.py)"; fail=1; }
|
|
170
|
+
fi
|
|
171
|
+
if [ -f scripts/migrate-learnings.py ]; then
|
|
172
|
+
python3 scripts/migrate-learnings.py --self-test >/dev/null \
|
|
173
|
+
|| { echo "FAIL: migrate-learnings self-test (personal-copy prune + in-bundle guard)"; fail=1; }
|
|
138
174
|
fi
|
|
139
175
|
|
|
140
176
|
# Lexicon gate: forbid deprecated terminology tokens in live files
|