Files
gstack/test/declared-annotation.test.ts
T
Garry Tan fa590c4f51 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>
2026-05-27 07:45:28 -07:00

130 lines
4.7 KiB
TypeScript

/**
* 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');
});
});