mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-16 09:52:22 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06eeb2890b |
@@ -233,13 +233,14 @@ keep it or `git checkout` to throw it away. Nothing is committed for you.
|
||||
|
||||
**For a skill that ships with gbrain** (anything under the gbrain repo's own
|
||||
`skills/`): SkillOpt refuses to overwrite it by default and writes the winner to
|
||||
`skills/<name>/skillopt/best.md` instead, so an optimization pass can never
|
||||
silently mutate a skill other people depend on. Two ways to handle that:
|
||||
`skills/<name>/skillopt/proposed.md` instead (while keeping `best.md` as the
|
||||
optimizer's current-best pointer), so an optimization pass can never silently
|
||||
mutate a skill other people depend on. Two ways to handle that:
|
||||
|
||||
```bash
|
||||
# See the proposed improvement without touching SKILL.md (works for ANY skill):
|
||||
gbrain skillopt meeting-prep --split 1:1:1 --no-mutate
|
||||
# → writes skills/meeting-prep/skillopt/best.md (the proposed rewrite), prints its path. Copy what you want.
|
||||
# → writes skills/meeting-prep/skillopt/proposed.md, updates best.md, and prints the proposal path.
|
||||
|
||||
# Actually rewrite a bundled skill (explicit opt-in + an independent held-out set):
|
||||
gbrain skillopt brain-ops --split 1:1:1 --allow-mutate-bundled \
|
||||
|
||||
@@ -35,11 +35,10 @@
|
||||
*
|
||||
* The doctor renders both side by side.
|
||||
*
|
||||
* Drift contract: every check name that ships through doctor MUST appear in
|
||||
* Drift contract: every check name that ships in doctor.ts MUST appear in
|
||||
* exactly one set below. The drift-guard test in
|
||||
* `test/doctor-categories.test.ts` enforces this by reading doctor check
|
||||
* emitter sources via a tagged-string scan and asserting set membership
|
||||
* exactly.
|
||||
* `test/doctor-categories.test.ts` enforces this by reading doctor.ts source
|
||||
* via a tagged-string scan and asserting set membership exactly.
|
||||
*
|
||||
* If you add a new doctor check, you MUST add its name to the appropriate
|
||||
* set here. The categorize step in `src/commands/doctor.ts` falls through
|
||||
@@ -68,15 +67,12 @@ export const BRAIN_CHECK_NAMES: ReadonlySet<string> = new Set([
|
||||
'conversation_parser_probe_health',
|
||||
'cross_modal_modality_backfill',
|
||||
'cycle_freshness',
|
||||
'dangling_aliases',
|
||||
'effective_date_health',
|
||||
'embed_staleness',
|
||||
'embedding_column_registry',
|
||||
'embedding_env_override',
|
||||
'embedding_provider',
|
||||
'embedding_width_consistency',
|
||||
'embeddings',
|
||||
'entity_link_coverage',
|
||||
'eval_drift',
|
||||
'extract_atoms_backlog',
|
||||
'extract_health',
|
||||
@@ -106,9 +102,7 @@ export const BRAIN_CHECK_NAMES: ReadonlySet<string> = new Set([
|
||||
'stub_guard_24h',
|
||||
'sync_failures',
|
||||
'sync_freshness',
|
||||
'takes_count',
|
||||
'takes_weight_grid',
|
||||
'timeline_coverage',
|
||||
'unified_multimodal_coverage',
|
||||
'voice_gate_health',
|
||||
]);
|
||||
@@ -176,14 +170,12 @@ export const META_CHECK_NAMES: ReadonlySet<string> = new Set([
|
||||
'eval_capture',
|
||||
'minions_migration',
|
||||
'multi_source_drift',
|
||||
'pack_upgrade_available',
|
||||
'schema_pack_active',
|
||||
'schema_pack_consistency',
|
||||
'schema_pack_source_drift',
|
||||
'schema_version',
|
||||
'slug_fallback_audit',
|
||||
'timeline_dedup_index',
|
||||
'type_proliferation',
|
||||
'upgrade_errors',
|
||||
]);
|
||||
|
||||
|
||||
@@ -93,7 +93,13 @@ import { resolveLrSchedule } from './lr-schedule.ts';
|
||||
import { preflight, formatPreflightReport } from './preflight.ts';
|
||||
import { isRejected, loadRejectedBuffer, makeRejectedEntry, saveRejectedBuffer } from './rejected-buffer.ts';
|
||||
import { runReflect, runOneShotRewrite, describeJudges } from './reflect.ts';
|
||||
import { acceptCandidate, bestPath, revertAllPending, skillPath, writeProposed } from './version-store.ts';
|
||||
import {
|
||||
acceptCandidate,
|
||||
proposedPath as proposedFilePath,
|
||||
revertAllPending,
|
||||
skillPath,
|
||||
writeProposed,
|
||||
} from './version-store.ts';
|
||||
import { runValidationGate, scoreSkillOnTasks } from './validate-gate.ts';
|
||||
import { ROLLOUT_SUCCESS_THRESHOLD } from './types.ts';
|
||||
import type { SkillOptOpts, EditOp, RunReceipt, BenchmarkTask } from './types.ts';
|
||||
@@ -702,9 +708,9 @@ async function runOptimizationLoop(
|
||||
// to the catch's assignment values only (it can't prove the async callback ran).
|
||||
const finalOutcome = outcome as 'accepted' | 'no_improvement' | 'aborted' | 'errored';
|
||||
if (!mutateDecision.mutate && finalOutcome === 'accepted') {
|
||||
// best.md was written by writeProposed() in the accept branch (no-mutate
|
||||
// path); it doubles as proposed.md for human review. SKILL.md untouched.
|
||||
proposedPath = bestPath(skillsDir, skillName);
|
||||
// writeProposed() emitted both the best pointer and the stable review
|
||||
// artifact in the accept branch. SKILL.md remains untouched.
|
||||
proposedPath = proposedFilePath(skillsDir, skillName);
|
||||
} else if (mutateDecision.mutate) {
|
||||
mutatedSkillFile = finalOutcome === 'accepted';
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*
|
||||
* history.json
|
||||
* best.md
|
||||
* proposed.md
|
||||
* versions/
|
||||
* v0001_e1_s1.md
|
||||
* v0002_e1_s2.md
|
||||
@@ -52,6 +53,10 @@ export function bestPath(skillsDir: string, skillName: string): string {
|
||||
return path.join(skilloptDir(skillsDir, skillName), 'best.md');
|
||||
}
|
||||
|
||||
export function proposedPath(skillsDir: string, skillName: string): string {
|
||||
return path.join(skilloptDir(skillsDir, skillName), 'proposed.md');
|
||||
}
|
||||
|
||||
export function skillPath(skillsDir: string, skillName: string): string {
|
||||
return path.join(skillsDir, skillName, 'SKILL.md');
|
||||
}
|
||||
@@ -171,17 +176,18 @@ export function acceptCandidate(input: AcceptInput): AcceptResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the candidate to `best.md` (which doubles as `proposed.md`) WITHOUT
|
||||
* touching SKILL.md or the history ledger. Used by the `--no-mutate` /
|
||||
* bundled-without-allow paths: the optimizer found a better candidate but the
|
||||
* caller opted out of in-place mutation, so we surface it for human review.
|
||||
* Returns the path written. Atomic (.tmp + rename).
|
||||
* Write the candidate to both `best.md` and `proposed.md` WITHOUT touching
|
||||
* SKILL.md or the history ledger. `best.md` remains the optimizer's current
|
||||
* best pointer; `proposed.md` is the stable human-review artifact promised by
|
||||
* `--no-mutate`. Returns the proposal path. Each write is atomic (.tmp + rename).
|
||||
*/
|
||||
export function writeProposed(skillsDir: string, skillName: string, candidateText: string): string {
|
||||
const p = bestPath(skillsDir, skillName);
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
atomicWrite(p, candidateText);
|
||||
return p;
|
||||
const best = bestPath(skillsDir, skillName);
|
||||
const proposed = proposedPath(skillsDir, skillName);
|
||||
fs.mkdirSync(path.dirname(best), { recursive: true });
|
||||
atomicWrite(best, candidateText);
|
||||
atomicWrite(proposed, candidateText);
|
||||
return proposed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Drift guard for src/core/doctor-categories.ts.
|
||||
*
|
||||
* Reads doctor check emitter source via a literal-string scan, enumerates every
|
||||
* `name: '<...>'` Check name, and asserts each appears in exactly ONE category
|
||||
* set. The union of the four sets must equal the discovered names exactly —
|
||||
* no orphans, no extras.
|
||||
* Reads src/commands/doctor.ts source via a literal-string scan, enumerates
|
||||
* every `name: '<...>'` Check name, and asserts each appears in exactly ONE
|
||||
* category set. The union of the four sets must equal the discovered names
|
||||
* exactly — no orphans, no extras.
|
||||
*
|
||||
* This is the structural failure the v0.41.19.0 plan-eng-review caught:
|
||||
* doctor.ts grows new checks regularly; without this guard, the
|
||||
@@ -25,30 +25,26 @@ import {
|
||||
} from '../src/core/doctor-categories.ts';
|
||||
|
||||
const DOCTOR_TS_PATH = join(import.meta.dir, '..', 'src', 'commands', 'doctor.ts');
|
||||
const ONBOARD_CHECKS_TS_PATH = join(import.meta.dir, '..', 'src', 'core', 'onboard', 'checks.ts');
|
||||
const CHECK_SOURCE_PATHS = [DOCTOR_TS_PATH, ONBOARD_CHECKS_TS_PATH];
|
||||
|
||||
function enumerateCheckNames(): Set<string> {
|
||||
const source = readFileSync(DOCTOR_TS_PATH, 'utf-8');
|
||||
const names = new Set<string>();
|
||||
for (const path of CHECK_SOURCE_PATHS) {
|
||||
const source = readFileSync(path, 'utf-8');
|
||||
// 1) Inline object-literal form: `{ name: 'foo', ... }`.
|
||||
for (const m of source.matchAll(/name:\s*['"]([a-z][a-z0-9_]+)['"]/g)) {
|
||||
names.add(m[1]);
|
||||
}
|
||||
// 2) Helper-function form: `const name = 'foo';` inside a check helper.
|
||||
// Catches checks like `nightly_quality_probe_health` and
|
||||
// `conversation_facts_backlog` that build the Check from a captured
|
||||
// name constant.
|
||||
for (const m of source.matchAll(/const\s+name\s*=\s*['"]([a-z][a-z0-9_]+)['"]/g)) {
|
||||
names.add(m[1]);
|
||||
}
|
||||
// 1) Inline object-literal form: `{ name: 'foo', ... }`.
|
||||
for (const m of source.matchAll(/name:\s*['"]([a-z][a-z0-9_]+)['"]/g)) {
|
||||
names.add(m[1]);
|
||||
}
|
||||
// 2) Helper-function form: `const name = 'foo';` inside a check helper.
|
||||
// Catches checks like `nightly_quality_probe_health` and
|
||||
// `conversation_facts_backlog` that build the Check from a captured
|
||||
// name constant.
|
||||
for (const m of source.matchAll(/const\s+name\s*=\s*['"]([a-z][a-z0-9_]+)['"]/g)) {
|
||||
names.add(m[1]);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
describe('doctor-categories drift guard', () => {
|
||||
test('every doctor-emitted check name belongs to exactly one category set', () => {
|
||||
test('every check name in doctor.ts source belongs to exactly one category set', () => {
|
||||
const discovered = enumerateCheckNames();
|
||||
const allCategorized = new Set<string>([
|
||||
...BRAIN_CHECK_NAMES,
|
||||
@@ -63,7 +59,7 @@ describe('doctor-categories drift guard', () => {
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`These check names appear in doctor check emitters but are not categorized in ` +
|
||||
`These check names appear in doctor.ts but are not categorized in ` +
|
||||
`src/core/doctor-categories.ts: ${missing.sort().join(', ')}. ` +
|
||||
`Add each to BRAIN/SKILL/OPS/META_CHECK_NAMES.`,
|
||||
);
|
||||
@@ -90,7 +86,7 @@ describe('doctor-categories drift guard', () => {
|
||||
expect(dupes).toEqual([]);
|
||||
});
|
||||
|
||||
test('every categorized name is currently used in doctor check emitters (no stale entries)', () => {
|
||||
test('every categorized name is currently used in doctor.ts source (no stale entries)', () => {
|
||||
const discovered = enumerateCheckNames();
|
||||
const allCategorized = new Set<string>([
|
||||
...BRAIN_CHECK_NAMES,
|
||||
@@ -128,14 +124,6 @@ describe('categorizeCheck', () => {
|
||||
expect(categorizeCheck('sync_freshness')).toBe('brain');
|
||||
});
|
||||
|
||||
test('returns the right category for onboard data-quality check names', () => {
|
||||
expect(categorizeCheck('embed_staleness')).toBe('brain');
|
||||
expect(categorizeCheck('entity_link_coverage')).toBe('brain');
|
||||
expect(categorizeCheck('timeline_coverage')).toBe('brain');
|
||||
expect(categorizeCheck('takes_count')).toBe('brain');
|
||||
expect(categorizeCheck('dangling_aliases')).toBe('brain');
|
||||
});
|
||||
|
||||
test('returns the right category for a known skill name', () => {
|
||||
expect(categorizeCheck('resolver_health')).toBe('skill');
|
||||
expect(categorizeCheck('skill_conformance')).toBe('skill');
|
||||
@@ -152,24 +140,6 @@ describe('categorizeCheck', () => {
|
||||
expect(categorizeCheck('upgrade_errors')).toBe('meta');
|
||||
});
|
||||
|
||||
test('returns the right category for onboard schema-pack check names without warning', () => {
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
const captured: string[] = [];
|
||||
(process.stderr as { write: typeof process.stderr.write }).write = ((
|
||||
chunk: string | Uint8Array,
|
||||
) => {
|
||||
captured.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
|
||||
return true;
|
||||
}) as typeof process.stderr.write;
|
||||
try {
|
||||
expect(categorizeCheck('pack_upgrade_available')).toBe('meta');
|
||||
expect(categorizeCheck('type_proliferation')).toBe('meta');
|
||||
expect(captured.filter((c) => c.includes('[doctor-categories]'))).toEqual([]);
|
||||
} finally {
|
||||
(process.stderr as { write: typeof process.stderr.write }).write = originalWrite;
|
||||
}
|
||||
});
|
||||
|
||||
test('unknown check name falls through to meta with a stderr warn (once per process)', () => {
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
const captured: string[] = [];
|
||||
|
||||
@@ -39,6 +39,7 @@ import { runSkillOpt } from '../../src/core/skillopt/orchestrator.ts';
|
||||
import {
|
||||
bestPath,
|
||||
loadHistory,
|
||||
proposedPath,
|
||||
skillPath,
|
||||
} from '../../src/core/skillopt/version-store.ts';
|
||||
import { loadRejectedBuffer } from '../../src/core/skillopt/rejected-buffer.ts';
|
||||
@@ -741,7 +742,7 @@ describe('skillopt T3 — F11 held-out gate, ablation opts, no-DB-pollution', ()
|
||||
} finally { fixture.cleanup(); }
|
||||
});
|
||||
|
||||
test('--no-mutate writes proposed.md (best.md), leaves SKILL.md untouched', async () => {
|
||||
test('--no-mutate writes proposed.md and best.md, leaves SKILL.md untouched', async () => {
|
||||
const fixture = setupFixture(SKILL_PEOPLE_ONLY, CITATIONS_BENCHMARK);
|
||||
try {
|
||||
installStub({
|
||||
@@ -753,10 +754,9 @@ describe('skillopt T3 — F11 held-out gate, ablation opts, no-DB-pollution', ()
|
||||
const result = await runOnce(fixture, { noMutate: true });
|
||||
expect(result.outcome).toBe('accepted');
|
||||
expect(result.mutatedSkillFile).toBe(false);
|
||||
expect(result.proposedPath).toBeDefined();
|
||||
// proposed.md (best.md) exists and carries the improvement.
|
||||
expect(fs.existsSync(result.proposedPath!)).toBe(true);
|
||||
expect(result.proposedPath).toBe(proposedPath(fixture.skillsDir, SKILL));
|
||||
expect(fs.readFileSync(result.proposedPath!, 'utf8')).toContain('## Citations');
|
||||
expect(fs.readFileSync(bestPath(fixture.skillsDir, SKILL), 'utf8')).toContain('## Citations');
|
||||
// SKILL.md on disk is UNCHANGED (still People-only).
|
||||
const skill = fs.readFileSync(skillPath(fixture.skillsDir, SKILL), 'utf8');
|
||||
expect(skill).not.toContain('## Citations');
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
bestPath,
|
||||
historyPath,
|
||||
loadHistory,
|
||||
proposedPath,
|
||||
revertAllPending,
|
||||
skillPath,
|
||||
versionsDir,
|
||||
writeProposed,
|
||||
} from '../../src/core/skillopt/version-store.ts';
|
||||
|
||||
let tmpDir: string;
|
||||
@@ -79,6 +81,19 @@ describe('acceptCandidate (D8 two-phase commit)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeProposed', () => {
|
||||
test('writes distinct best and proposed artifacts without mutating SKILL.md (#2635)', () => {
|
||||
const candidate = '---\nname: test\n---\nproposed body\n';
|
||||
|
||||
const written = writeProposed(tmpDir, SKILL, candidate);
|
||||
|
||||
expect(written).toBe(proposedPath(tmpDir, SKILL));
|
||||
expect(fs.readFileSync(bestPath(tmpDir, SKILL), 'utf8')).toBe(candidate);
|
||||
expect(fs.readFileSync(proposedPath(tmpDir, SKILL), 'utf8')).toBe(candidate);
|
||||
expect(fs.readFileSync(skillPath(tmpDir, SKILL), 'utf8')).toContain('baseline body');
|
||||
});
|
||||
});
|
||||
|
||||
describe('revertAllPending (D8 crash recovery)', () => {
|
||||
test('no-op when no pending rows', () => {
|
||||
const reverted = revertAllPending(tmpDir, SKILL);
|
||||
|
||||
Reference in New Issue
Block a user