mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-26 03:30:05 +02:00
feat(scripts): declared-annotation helper + autonomy signal_key wiring
Plan-tune cathedral T7. Adds the helper that lets skills inject one-line plain-English annotations on AUQ recommendations based on the user's declared profile — read-only, advisory-only, per TODOS.md E1 substrate-risk guidance (no AUTO_DECIDE off inferred). scripts/declared-annotation.ts - getDeclaredAnnotation(signal_key) → annotation | null - primaryDimensionFor(signal_key) → Dimension | null - Signature uses kebab signal_key per D2/Codex correction (registry uses hyphens; profile dimensions use underscores; helper maps internally). - Bands: >= 0.7 high, <= 0.3 low, else null. Middle band stays silent. - Per-dimension plain-English phrasing: 5 dimensions × 2 bands = 10 phrases. - Reads ~/.gstack/developer-profile.json (honors GSTACK_STATE_ROOT). scripts/psychographic-signals.ts - New signal_key 'decision-autonomy' that maps user_choice → autonomy dimension nudges. This was the missing signal for the 'autonomy' dimension — without it, the cathedral could annotate four of five declared dimensions but autonomy stayed silent. scripts/question-registry.ts - Add signal_key: 'decision-autonomy' to land-and-deploy-merge-confirm and land-and-deploy-rollback. These are the highest-leverage autonomy questions in the surface — "let me decide" vs "go ahead" is exactly what the dimension captures. 13 unit tests cover the helper's full contract (unknown keys, missing profile, middle-band null, both band thresholds, all five dimensions rendering distinct phrases). Existing 47 plan-tune.test.ts tests still pass after the registry + signal-map enrichment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Declared annotation helper (plan-tune cathedral T7) — unit tests.
|
||||
*
|
||||
* Verifies the helper's contract:
|
||||
* - Returns null for unknown signal_key.
|
||||
* - Returns null when the profile doesn't exist or declared is unset.
|
||||
* - Returns a phrase when declared >= 0.7 (strong high band).
|
||||
* - Returns a phrase when declared <= 0.3 (strong low band).
|
||||
* - Returns null when declared is in the middle band (0.3 < x < 0.7).
|
||||
* - primaryDimensionFor picks the dimension with largest |delta| total.
|
||||
* - Maps kebab signal_key to underscore Dimension correctly (D2 fix).
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
import { getDeclaredAnnotation, primaryDimensionFor } from '../scripts/declared-annotation';
|
||||
|
||||
let prevStateRoot: string | undefined;
|
||||
let prevHome: string | undefined;
|
||||
let stateRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
stateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-annot-'));
|
||||
prevStateRoot = process.env.GSTACK_STATE_ROOT;
|
||||
prevHome = process.env.GSTACK_HOME;
|
||||
process.env.GSTACK_STATE_ROOT = stateRoot;
|
||||
delete process.env.GSTACK_HOME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (prevStateRoot !== undefined) process.env.GSTACK_STATE_ROOT = prevStateRoot;
|
||||
else delete process.env.GSTACK_STATE_ROOT;
|
||||
if (prevHome !== undefined) process.env.GSTACK_HOME = prevHome;
|
||||
fs.rmSync(stateRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeProfile(declared: Record<string, number>): void {
|
||||
const p = path.join(stateRoot, 'developer-profile.json');
|
||||
fs.writeFileSync(p, JSON.stringify({ declared }, null, 2));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// primaryDimensionFor — kebab→underscore mapping
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
describe('primaryDimensionFor', () => {
|
||||
test('scope-appetite → scope_appetite (largest |delta| total)', () => {
|
||||
expect(primaryDimensionFor('scope-appetite')).toBe('scope_appetite');
|
||||
});
|
||||
|
||||
test('architecture-care → architecture_care (top dim by |delta|)', () => {
|
||||
expect(primaryDimensionFor('architecture-care')).toBe('architecture_care');
|
||||
});
|
||||
|
||||
test('unknown signal_key → null', () => {
|
||||
expect(primaryDimensionFor('totally-not-a-key')).toBe(null);
|
||||
});
|
||||
|
||||
test('empty/garbage input → null', () => {
|
||||
expect(primaryDimensionFor('')).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// getDeclaredAnnotation
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
describe('getDeclaredAnnotation', () => {
|
||||
test('returns null when no profile exists', () => {
|
||||
expect(getDeclaredAnnotation('scope-appetite')).toBe(null);
|
||||
});
|
||||
|
||||
test('returns null when declared unset for the dimension', () => {
|
||||
writeProfile({});
|
||||
expect(getDeclaredAnnotation('scope-appetite')).toBe(null);
|
||||
});
|
||||
|
||||
test('returns null when declared is in middle band (0.5)', () => {
|
||||
writeProfile({ scope_appetite: 0.5 });
|
||||
expect(getDeclaredAnnotation('scope-appetite')).toBe(null);
|
||||
});
|
||||
|
||||
test('returns high-band phrase when declared >= 0.7', () => {
|
||||
writeProfile({ scope_appetite: 0.85 });
|
||||
const annot = getDeclaredAnnotation('scope-appetite');
|
||||
expect(annot).toBeTruthy();
|
||||
expect(annot).toContain('boil the ocean');
|
||||
});
|
||||
|
||||
test('returns high-band phrase at the exact 0.7 threshold', () => {
|
||||
writeProfile({ scope_appetite: 0.7 });
|
||||
expect(getDeclaredAnnotation('scope-appetite')).toContain('boil the ocean');
|
||||
});
|
||||
|
||||
test('returns low-band phrase when declared <= 0.3', () => {
|
||||
writeProfile({ scope_appetite: 0.2 });
|
||||
const annot = getDeclaredAnnotation('scope-appetite');
|
||||
expect(annot).toBeTruthy();
|
||||
expect(annot).toContain('ship-small-fast');
|
||||
});
|
||||
|
||||
test('returns low-band phrase at the exact 0.3 threshold', () => {
|
||||
writeProfile({ scope_appetite: 0.3 });
|
||||
expect(getDeclaredAnnotation('scope-appetite')).toContain('ship-small-fast');
|
||||
});
|
||||
|
||||
test('returns null for unknown signal_key even when profile populated', () => {
|
||||
writeProfile({ scope_appetite: 0.85 });
|
||||
expect(getDeclaredAnnotation('totally-not-a-key')).toBe(null);
|
||||
});
|
||||
|
||||
test('all 5 dimensions render distinct high-band phrases', () => {
|
||||
// Use the 5 signal_keys known to map to each of the 5 dimensions.
|
||||
writeProfile({
|
||||
scope_appetite: 0.9,
|
||||
risk_tolerance: 0.9,
|
||||
detail_preference: 0.9,
|
||||
autonomy: 0.9,
|
||||
architecture_care: 0.9,
|
||||
});
|
||||
const scope = getDeclaredAnnotation('scope-appetite');
|
||||
const arch = getDeclaredAnnotation('architecture-care');
|
||||
expect(scope).toContain('boil the ocean');
|
||||
expect(arch).toContain('design-right');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user