Files
waggle-os/docs/superpowers/plans/2026-06-28-ai-os-positioning-audit.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

804 lines
30 KiB
Markdown

# AI OS Positioning Audit Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a report-mode Playwright E2E audit that grades Waggle across five personas for AI OS positioning, addiction/return signals, competitor comparability, and prioritized improvement areas.
**Architecture:** Add one self-contained Playwright spec that probes the existing app shell and API surfaces, scores deterministic evidence, and writes Markdown plus JSON artifacts through Playwright's per-test output directory. The audit passes when report generation succeeds and records low product scores as findings, not test failures.
**Tech Stack:** TypeScript, Playwright test runner, Node `fs/promises`, existing `WAGGLE_E2E_BASE_URL`, existing local server from `playwright.config.ts` or `playwright-e2e.config.ts`.
---
## File Structure
- Create: `tests/e2e/ai-os-positioning-audit.spec.ts`
- Owns persona definitions, endpoint probes, scoring functions, report rendering, artifact writing, and the Playwright test.
- No app/product files change.
- No committed report artifacts.
- Runtime output goes under `testInfo.outputPath('ai-os-positioning-audit.md')` and `testInfo.outputPath('ai-os-positioning-audit.json')`.
- Existing docs remain unchanged after this plan:
- Spec source: `docs/superpowers/specs/2026-06-28-ai-os-positioning-audit-design.md`
---
### Task 1: RED - Add Audit Contract Spec Shell
**Files:**
- Create: `tests/e2e/ai-os-positioning-audit.spec.ts`
- [ ] **Step 1: Write the failing report contract test**
Create `tests/e2e/ai-os-positioning-audit.spec.ts` with this initial content:
```typescript
import { expect, test } from '@playwright/test';
test.describe('AI OS positioning audit', () => {
test('generates a report-mode audit with five personas and improvement areas', async ({ page }, testInfo) => {
const audit = await runAiOsPositioningAudit(page, testInfo);
expect(audit.personas).toHaveLength(5);
expect(audit.overall.score).toBeGreaterThanOrEqual(0);
expect(audit.overall.score).toBeLessThanOrEqual(100);
expect(audit.overall.grade).toMatch(/AI OS|chat|niche|promising|plausible/i);
expect(audit.addictionLevel).toMatch(/weak|emerging|strong|very strong/i);
expect(audit.improvementAreas.length).toBeGreaterThan(0);
expect(audit.artifacts.markdownPath).toMatch(/ai-os-positioning-audit\.md$/);
expect(audit.artifacts.jsonPath).toMatch(/ai-os-positioning-audit\.json$/);
});
});
```
- [ ] **Step 2: Run the spec to verify RED**
Run:
```bash
node node_modules/playwright/cli.js test tests/e2e/ai-os-positioning-audit.spec.ts --project=chromium --reporter=list
```
Expected: FAIL before implementation, with a TypeScript/runtime error equivalent to `runAiOsPositioningAudit is not defined`.
- [ ] **Step 3: Commit nothing yet**
Do not commit the red state. Continue to Task 2.
---
### Task 2: GREEN - Add Data Model and Report Renderer
**Files:**
- Modify: `tests/e2e/ai-os-positioning-audit.spec.ts`
- [ ] **Step 1: Add imports, types, personas, and constants above the test**
Replace the file contents with the contract test plus these declarations above it:
```typescript
import { expect, type APIRequestContext, type Page, test, type TestInfo } from '@playwright/test';
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
const BASE = process.env.WAGGLE_E2E_BASE_URL ?? 'http://127.0.0.1:3333';
const SKIP = 'skipOnboarding=true&skipBoot=true&tier=power&skipBriefing=true';
type DimensionId =
| 'onboarding'
| 'timeToValue'
| 'memory'
| 'workflowCoverage'
| 'competitiveAdvantage'
| 'addiction';
interface PersonaDefinition {
id: string;
name: string;
role: string;
currentDefault: string[];
jobToBeDone: string;
oneToolCriterion: string;
memoryAnchor: string;
memoryQuery: string;
expectedPersonaIds: string[];
expectedSkillTerms: string[];
expectedConnectorTerms: string[];
externalTriggerNeed: string;
internalTriggerNeed: string;
competitorBaseline: string;
}
interface DimensionScore {
id: DimensionId;
label: string;
max: number;
score: number;
evidence: string[];
gaps: string[];
}
interface PersonaScore {
id: string;
name: string;
role: string;
total: number;
grade: string;
positioning: string;
currentDefault: string[];
competitorBaseline: string;
oneToolCriterion: string;
dimensions: DimensionScore[];
improvementAreas: string[];
}
interface ImprovementArea {
priority: number;
personaId: string;
personaName: string;
dimension: string;
impact: number;
recommendation: string;
evidence: string[];
}
interface AuditResult {
generatedAt: string;
overall: {
score: number;
grade: string;
positioningVerdict: string;
};
addictionLevel: string;
personas: PersonaScore[];
improvementAreas: ImprovementArea[];
artifacts: {
markdownPath: string;
jsonPath: string;
};
}
interface ProbeContext {
shellLoaded: boolean;
shellText: string;
consoleErrors: string[];
api: Record<string, { status: number; ok: boolean; ms: number; body: unknown }>;
personaIds: string[];
personaText: string;
skillsText: string;
connectorsText: string;
marketplaceText: string;
memory: Record<string, { saved: boolean; recalled: boolean; isolated: boolean; evidence: string[] }>;
}
const PERSONAS: PersonaDefinition[] = [
{
id: 'sofia-operator',
name: 'Sofia',
role: 'Small business operator',
currentDefault: ['ChatGPT', 'Gmail', 'Canva'],
jobToBeDone: 'Draft customer replies, campaign ideas, and supplier follow-ups.',
oneToolCriterion: 'Daily communications and decisions happen in Waggle.',
memoryAnchor: 'Sofia runs a neighborhood studio and prefers short, warm customer replies with clear next steps.',
memoryQuery: 'short warm customer replies next steps',
expectedPersonaIds: ['support-agent', 'marketer', 'executive-assistant'],
expectedSkillTerms: ['email', 'marketing', 'document'],
expectedConnectorTerms: ['gmail', 'google', 'slack'],
externalTriggerNeed: 'daily brief or reply reminder',
internalTriggerNeed: 'I need to answer customers without sounding generic',
competitorBaseline: 'ChatGPT is fast for drafting but does not own her customer context or operating rhythm.',
},
{
id: 'mara-writer',
name: 'Mara',
role: 'Marketing writer',
currentDefault: ['ChatGPT', 'Claude', 'Notion AI'],
jobToBeDone: 'Turn notes and research into branded copy.',
oneToolCriterion: 'Voice, drafts, and campaign memory compound in Waggle.',
memoryAnchor: 'Mara writes in a crisp, specific brand voice and tracks campaign decisions by launch.',
memoryQuery: 'brand voice campaign decisions launch',
expectedPersonaIds: ['writer', 'marketer', 'creative-director'],
expectedSkillTerms: ['writing', 'brand', 'markdown'],
expectedConnectorTerms: ['notion', 'google', 'slack'],
externalTriggerNeed: 'weekly wins digest or draft reminder',
internalTriggerNeed: 'I need the AI to remember my voice and the campaign angle',
competitorBaseline: 'Claude is excellent at prose but does not act as a durable operating workspace.',
},
{
id: 'imran-consultant',
name: 'Imran',
role: 'Independent consultant',
currentDefault: ['Claude', 'ChatGPT', 'Gamma'],
jobToBeDone: 'Convert calls and notes into frameworks, briefs, and follow-ups.',
oneToolCriterion: 'Client context and recurring strategy work live in Waggle.',
memoryAnchor: 'Imran uses 2x2 frameworks and wants every client decision remembered by account.',
memoryQuery: '2x2 framework client decision account',
expectedPersonaIds: ['consultant', 'researcher', 'analyst'],
expectedSkillTerms: ['presentation', 'document', 'research'],
expectedConnectorTerms: ['calendar', 'google', 'slack'],
externalTriggerNeed: 'client follow-up reminder',
internalTriggerNeed: 'What did we decide for this client last time?',
competitorBaseline: 'Claude and Gamma help produce artifacts, but the client memory loop is fragmented.',
},
{
id: 'daniel-finance',
name: 'Daniel',
role: 'Finance and operations analyst',
currentDefault: ['Excel Copilot', 'ChatGPT', 'Looker'],
jobToBeDone: 'Explain variance, summarize metrics, and prepare board commentary.',
oneToolCriterion: 'Data commentary and recurring monthly memory live in Waggle.',
memoryAnchor: 'Daniel prepares monthly board commentary and cares about variance drivers, ARR, NPS, and burn.',
memoryQuery: 'monthly board commentary variance ARR NPS burn',
expectedPersonaIds: ['finance-owner', 'analyst', 'ops-manager'],
expectedSkillTerms: ['spreadsheet', 'csv', 'analysis'],
expectedConnectorTerms: ['excel', 'google', 'microsoft'],
externalTriggerNeed: 'monthly reporting reminder',
internalTriggerNeed: 'I need a variance explanation I can defend',
competitorBaseline: 'Excel Copilot is close to the data but weak as cross-month memory and agent workspace.',
},
{
id: 'priya-power-user',
name: 'Priya',
role: 'AI power user',
currentDefault: ['Claude Code', 'Codex', 'Hermes', 'OpenClaw'],
jobToBeDone: 'Coordinate AI workflows, skills, connectors, and memory.',
oneToolCriterion: 'Waggle is the front door for non-coding agent work.',
memoryAnchor: 'Priya wants a non-coding AI command center with skills, connectors, memory, and agent coordination.',
memoryQuery: 'non coding command center skills connectors agent coordination',
expectedPersonaIds: ['coordinator', 'planner', 'verifier', 'coder'],
expectedSkillTerms: ['skill', 'automation', 'agent'],
expectedConnectorTerms: ['github', 'mcp', 'webhook'],
externalTriggerNeed: 'OS hotkey, launcher, or scheduled automation',
internalTriggerNeed: 'I need one control surface for all my AI work',
competitorBaseline: 'Developer tools are powerful for code but do not give a non-coding AI OS cockpit.',
},
];
```
- [ ] **Step 2: Add report helper functions below the constants**
Add these helpers:
```typescript
function clampScore(score: number, max: number): number {
return Math.max(0, Math.min(max, Math.round(score)));
}
function gradeFor(score: number): string {
if (score >= 90) return 'Strong AI OS position';
if (score >= 75) return 'Strong niche AI OS fit';
if (score >= 60) return 'Promising but still competitor-dependent';
if (score >= 40) return 'Plausible positioning, weak product proof';
return 'Likely perceived as another AI chat/tool wrapper';
}
function addictionLevelFor(score: number): string {
if (score >= 85) return 'very strong';
if (score >= 70) return 'strong';
if (score >= 50) return 'emerging';
return 'weak';
}
function textIncludesAny(haystack: string, needles: string[]): boolean {
const lower = haystack.toLowerCase();
return needles.some((needle) => lower.includes(needle.toLowerCase()));
}
function renderMarkdown(audit: AuditResult): string {
const lines: string[] = [];
lines.push('# Waggle AI OS Positioning Audit');
lines.push('');
lines.push(`Generated: ${audit.generatedAt}`);
lines.push(`Overall score: ${audit.overall.score}/100`);
lines.push(`Grade: ${audit.overall.grade}`);
lines.push(`AI OS verdict: ${audit.overall.positioningVerdict}`);
lines.push(`Addiction level: ${audit.addictionLevel}`);
lines.push('');
lines.push('## Persona Scores');
lines.push('');
lines.push('| Persona | Role | Score | Grade | Current default |');
lines.push('|---|---|---:|---|---|');
for (const persona of audit.personas) {
lines.push(`| ${persona.name} | ${persona.role} | ${persona.total} | ${persona.grade} | ${persona.currentDefault.join(', ')} |`);
}
lines.push('');
lines.push('## Improvement Areas');
lines.push('');
for (const item of audit.improvementAreas) {
lines.push(`${item.priority}. **${item.personaName} - ${item.dimension}** (${item.impact} pts): ${item.recommendation}`);
for (const evidence of item.evidence.slice(0, 2)) {
lines.push(` - Evidence: ${evidence}`);
}
}
lines.push('');
lines.push('## Persona Detail');
for (const persona of audit.personas) {
lines.push('');
lines.push(`### ${persona.name} - ${persona.role}`);
lines.push('');
lines.push(`Score: ${persona.total}/100`);
lines.push(`Positioning: ${persona.positioning}`);
lines.push(`One-tool criterion: ${persona.oneToolCriterion}`);
lines.push(`Competitor baseline: ${persona.competitorBaseline}`);
lines.push('');
lines.push('| Dimension | Score | Evidence | Gaps |');
lines.push('|---|---:|---|---|');
for (const dim of persona.dimensions) {
lines.push(`| ${dim.label} | ${dim.score}/${dim.max} | ${dim.evidence.join('<br>')} | ${dim.gaps.join('<br>')} |`);
}
}
lines.push('');
return `${lines.join('\n')}\n`;
}
```
- [ ] **Step 3: Run the spec**
Run:
```bash
node node_modules/playwright/cli.js test tests/e2e/ai-os-positioning-audit.spec.ts --project=chromium --reporter=list
```
Expected: still FAIL because `runAiOsPositioningAudit` is not implemented. This keeps the red loop honest while type definitions and rendering are in place.
---
### Task 3: GREEN - Add Deterministic Probes
**Files:**
- Modify: `tests/e2e/ai-os-positioning-audit.spec.ts`
- [ ] **Step 1: Add endpoint and browser probe helpers below report helpers**
Add:
```typescript
async function timedGet(request: APIRequestContext, path: string) {
const started = Date.now();
const response = await request.get(`${BASE}${path}`).catch(() => null);
const ms = Date.now() - started;
if (!response) return { status: 0, ok: false, ms, body: null };
const body = await response.json().catch(async () => response.text().catch(() => null));
return { status: response.status(), ok: response.ok(), ms, body };
}
async function saveMemory(request: APIRequestContext, workspace: string, content: string): Promise<boolean> {
const response = await request.post(`${BASE}/api/memory/frames`, {
data: { content, workspace, source: 'user_stated', importance: 'normal' },
}).catch(() => null);
return !!response?.ok();
}
async function searchMemory(request: APIRequestContext, workspace: string, query: string): Promise<{ found: boolean; evidence: string[] }> {
const response = await request.get(
`${BASE}/api/memory/search?q=${encodeURIComponent(query)}&workspace=${encodeURIComponent(workspace)}&limit=5`,
).catch(() => null);
if (!response) return { found: false, evidence: ['Memory search did not return a response.'] };
if (!response.ok()) return { found: false, evidence: [`Memory search returned ${response.status()}.`] };
const body = await response.json().catch(() => ({}));
const raw = JSON.stringify(body);
const results = body.results ?? body.recalled ?? [];
return {
found: Array.isArray(results) ? results.length > 0 || raw.toLowerCase().includes(query.split(' ')[0].toLowerCase()) : raw.length > 20,
evidence: [`Memory search returned ${Array.isArray(results) ? results.length : 'unknown'} result(s).`],
};
}
async function probeMemory(request: APIRequestContext, persona: PersonaDefinition) {
const workspace = `ai-os-audit-${persona.id}-${Date.now()}`;
const otherWorkspace = `${workspace}-other`;
const saved = await saveMemory(request, workspace, persona.memoryAnchor);
await new Promise((resolve) => setTimeout(resolve, 200));
const recall = await searchMemory(request, workspace, persona.memoryQuery);
const isolation = await searchMemory(request, otherWorkspace, persona.memoryAnchor);
return {
saved,
recalled: saved && recall.found,
isolated: !isolation.found,
evidence: [
saved ? 'Persona memory anchor saved.' : 'Persona memory anchor could not be saved.',
...recall.evidence,
isolation.found ? 'Potential cross-workspace memory leakage detected.' : 'No cross-workspace recall detected for the persona anchor.',
],
};
}
async function collectProbeContext(page: Page): Promise<ProbeContext> {
const consoleErrors: string[] = [];
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text());
});
await page.goto(`${BASE}/home?${SKIP}`, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.waggle-sidebar, [role="navigation"], main', { timeout: 15_000 }).catch(() => {});
await page.waitForTimeout(600);
const shellText = await page.locator('body').innerText().catch(() => '');
const shellLoaded = shellText.length > 80 && !/error boundary|something went wrong/i.test(shellText);
const endpoints = {
health: '/health',
personas: '/api/personas',
skills: '/api/skills',
connectors: '/api/connectors',
marketplace: '/api/marketplace/search?query=&limit=10',
workspaces: '/api/workspaces',
hooks: '/api/hooks',
fleet: '/api/fleet',
events: '/api/events?limit=3',
tier: '/api/tier',
};
const apiEntries = await Promise.all(
Object.entries(endpoints).map(async ([key, path]) => [key, await timedGet(page.request, path)] as const),
);
const api = Object.fromEntries(apiEntries);
const personaBody = api.personas?.body as { personas?: Array<{ id?: string; name?: string; description?: string }> } | unknown[];
const personaRows = Array.isArray(personaBody) ? personaBody : Array.isArray(personaBody?.personas) ? personaBody.personas : [];
const personaIds = personaRows.map((persona) => String(persona.id ?? ''));
const memory: ProbeContext['memory'] = {};
for (const persona of PERSONAS) {
memory[persona.id] = await probeMemory(page.request, persona);
}
return {
shellLoaded,
shellText,
consoleErrors,
api,
personaIds,
personaText: JSON.stringify(api.personas?.body ?? ''),
skillsText: JSON.stringify(api.skills?.body ?? ''),
connectorsText: JSON.stringify(api.connectors?.body ?? ''),
marketplaceText: JSON.stringify(api.marketplace?.body ?? ''),
memory,
};
}
```
- [ ] **Step 2: Run the spec**
Run:
```bash
node node_modules/playwright/cli.js test tests/e2e/ai-os-positioning-audit.spec.ts --project=chromium --reporter=list
```
Expected: still FAIL because `runAiOsPositioningAudit` is not implemented.
---
### Task 4: GREEN - Add Scoring and Artifact Writing
**Files:**
- Modify: `tests/e2e/ai-os-positioning-audit.spec.ts`
- [ ] **Step 1: Add scoring helpers below probe helpers**
Add:
```typescript
function scorePersona(persona: PersonaDefinition, context: ProbeContext): PersonaScore {
const memory = context.memory[persona.id];
const fastCoreApis = ['health', 'personas', 'workspaces'].filter((key) => context.api[key]?.ok && context.api[key].ms < 800);
const relevantPersona = persona.expectedPersonaIds.some((id) => context.personaIds.includes(id));
const relevantSkills = textIncludesAny(`${context.skillsText} ${context.marketplaceText}`, persona.expectedSkillTerms);
const relevantConnectors = textIncludesAny(context.connectorsText, persona.expectedConnectorTerms);
const osSurfaces = ['hooks', 'fleet', 'events', 'tier'].filter((key) => context.api[key]?.ok || [403, 404].includes(context.api[key]?.status ?? 0));
const dimensions: DimensionScore[] = [
{
id: 'onboarding',
label: 'Onboarding clarity',
max: 15,
score: clampScore((context.shellLoaded ? 10 : 0) + (context.consoleErrors.length === 0 ? 3 : 0) + (/Waggle|workspace|AI/i.test(context.shellText) ? 2 : 0), 15),
evidence: [
context.shellLoaded ? 'App shell loaded meaningful content.' : 'App shell did not load meaningful content.',
`${context.consoleErrors.length} console error(s) captured on first load.`,
],
gaps: [
...(context.shellLoaded ? [] : ['Make first-load shell resilient and clearly explain what Waggle is.']),
...(/AI OS|operating system/i.test(context.shellText) ? [] : ['AI OS positioning is not explicit in the loaded shell text.']),
],
},
{
id: 'timeToValue',
label: 'Time to first value',
max: 15,
score: clampScore(fastCoreApis.length * 4 + (context.api.marketplace?.ok ? 3 : 0), 15),
evidence: fastCoreApis.map((key) => `${key} responded in ${context.api[key].ms}ms.`),
gaps: fastCoreApis.length >= 3 ? [] : ['Core first-value APIs should respond quickly and consistently.'],
},
{
id: 'memory',
label: 'Memory and continuity',
max: 20,
score: clampScore((memory?.saved ? 7 : 0) + (memory?.recalled ? 8 : 0) + (memory?.isolated ? 5 : 0), 20),
evidence: memory?.evidence ?? ['Memory probe did not run.'],
gaps: [
...(memory?.saved ? [] : ['Memory anchor save failed.']),
...(memory?.recalled ? [] : ['Saved memory was not confidently recalled.']),
...(memory?.isolated ? [] : ['Workspace isolation needs clearer proof.']),
],
},
{
id: 'workflowCoverage',
label: 'Workflow coverage',
max: 15,
score: clampScore((relevantPersona ? 5 : 0) + (relevantSkills ? 5 : 0) + (relevantConnectors ? 5 : 0), 15),
evidence: [
relevantPersona ? 'Relevant persona is present.' : `Missing obvious persona match from ${persona.expectedPersonaIds.join(', ')}.`,
relevantSkills ? 'Relevant skill or marketplace language found.' : `No clear skill match for ${persona.expectedSkillTerms.join(', ')}.`,
relevantConnectors ? 'Relevant connector language found.' : `No clear connector match for ${persona.expectedConnectorTerms.join(', ')}.`,
],
gaps: [
...(relevantPersona ? [] : ['Add or surface a persona that matches this workflow.']),
...(relevantSkills ? [] : ['Improve skill/template coverage for this workflow.']),
...(relevantConnectors ? [] : ['Improve connector coverage or setup guidance for this workflow.']),
],
},
{
id: 'competitiveAdvantage',
label: 'Competitive advantage',
max: 15,
score: clampScore((memory?.recalled ? 5 : 0) + (osSurfaces.length >= 3 ? 5 : 0) + (relevantPersona && relevantSkills ? 5 : 0), 15),
evidence: [
persona.competitorBaseline,
`${osSurfaces.length}/4 OS-like surfaces responded or degraded gracefully.`,
],
gaps: osSurfaces.length >= 3 && memory?.recalled ? [] : ['Make the advantage over the current default more visible and more provable.'],
},
{
id: 'addiction',
label: 'Addiction/return signal',
max: 20,
score: clampScore((memory?.recalled ? 7 : 0) + (context.api.events?.ok ? 3 : 0) + (context.api.hooks?.ok ? 4 : 0) + (context.api.workspaces?.ok ? 3 : 0) + (context.api.tier?.ok ? 3 : 0), 20),
evidence: [
`External trigger need: ${persona.externalTriggerNeed}.`,
`Internal trigger need: ${persona.internalTriggerNeed}.`,
],
gaps: [
...(context.api.hooks?.ok ? [] : ['Durable external trigger surface is weak or not reachable.']),
...(memory?.recalled ? [] : ['Stored value is not strong enough to create a return habit.']),
],
},
];
const total = dimensions.reduce((sum, dimension) => sum + dimension.score, 0);
return {
id: persona.id,
name: persona.name,
role: persona.role,
total,
grade: gradeFor(total),
positioning: total >= 75 ? 'Can credibly position Waggle as an AI OS for this persona.' : 'Needs sharper proof before AI OS positioning will feel earned.',
currentDefault: persona.currentDefault,
competitorBaseline: persona.competitorBaseline,
oneToolCriterion: persona.oneToolCriterion,
dimensions,
improvementAreas: dimensions.flatMap((dimension) => dimension.gaps.map((gap) => `${dimension.label}: ${gap}`)),
};
}
function collectImprovementAreas(personas: PersonaScore[]): ImprovementArea[] {
const areas: ImprovementArea[] = [];
for (const persona of personas) {
for (const dimension of persona.dimensions) {
const impact = dimension.max - dimension.score;
if (impact <= 0) continue;
areas.push({
priority: 0,
personaId: persona.id,
personaName: persona.name,
dimension: dimension.label,
impact,
recommendation: dimension.gaps[0] ?? `Improve ${dimension.label.toLowerCase()} for ${persona.name}.`,
evidence: dimension.evidence,
});
}
}
return areas
.sort((a, b) => b.impact - a.impact || a.personaName.localeCompare(b.personaName))
.slice(0, 12)
.map((area, index) => ({ ...area, priority: index + 1 }));
}
function positioningVerdict(score: number): string {
if (score >= 85) return 'Waggle can lead with AI OS positioning now, with persona-specific proof.';
if (score >= 70) return 'Waggle has credible AI OS positioning for selected niches, but first-session proof must sharpen.';
if (score >= 55) return 'Waggle should position as a memory-native AI workspace before claiming full AI OS broadly.';
return 'Waggle should fix core value proof before using AI OS as the main market claim.';
}
```
- [ ] **Step 2: Add the main audit runner below scoring helpers**
Add:
```typescript
async function runAiOsPositioningAudit(page: Page, testInfo: TestInfo): Promise<AuditResult> {
const context = await collectProbeContext(page);
const personas = PERSONAS.map((persona) => scorePersona(persona, context));
const overallScore = Math.round(personas.reduce((sum, persona) => sum + persona.total, 0) / personas.length);
const generatedAt = new Date().toISOString();
const markdownPath = testInfo.outputPath('ai-os-positioning-audit.md');
const jsonPath = testInfo.outputPath('ai-os-positioning-audit.json');
const audit: AuditResult = {
generatedAt,
overall: {
score: overallScore,
grade: gradeFor(overallScore),
positioningVerdict: positioningVerdict(overallScore),
},
addictionLevel: addictionLevelFor(Math.round(
personas.reduce((sum, persona) => {
const addiction = persona.dimensions.find((dimension) => dimension.id === 'addiction');
return sum + (addiction ? (addiction.score / addiction.max) * 100 : 0);
}, 0) / personas.length,
)),
personas,
improvementAreas: collectImprovementAreas(personas),
artifacts: {
markdownPath,
jsonPath,
},
};
const markdown = renderMarkdown(audit);
await mkdir(dirname(markdownPath), { recursive: true });
await writeFile(markdownPath, markdown, 'utf8');
await writeFile(jsonPath, JSON.stringify(audit, null, 2), 'utf8');
await testInfo.attach('ai-os-positioning-audit.md', { path: markdownPath, contentType: 'text/markdown' });
await testInfo.attach('ai-os-positioning-audit.json', { path: jsonPath, contentType: 'application/json' });
return audit;
}
```
- [ ] **Step 3: Run the spec to verify GREEN**
Run:
```bash
node node_modules/playwright/cli.js test tests/e2e/ai-os-positioning-audit.spec.ts --project=chromium --reporter=list
```
Expected: PASS, with output showing one passing test and Playwright attachments for Markdown and JSON.
---
### Task 5: Refactor and Harden Audit Behavior
**Files:**
- Modify: `tests/e2e/ai-os-positioning-audit.spec.ts`
- [ ] **Step 1: Add assertions that low scores do not fail the audit**
In the existing test body, keep the score range assertions but do not assert minimum product score. Ensure the only product-score assertions are:
```typescript
expect(audit.overall.score).toBeGreaterThanOrEqual(0);
expect(audit.overall.score).toBeLessThanOrEqual(100);
```
- [ ] **Step 2: Add artifact content assertions to the same test**
Add:
```typescript
const markdown = await testInfo.outputPath('ai-os-positioning-audit.md');
expect(markdown).toMatch(/ai-os-positioning-audit\.md$/);
for (const persona of ['Sofia', 'Mara', 'Imran', 'Daniel', 'Priya']) {
expect(audit.personas.map((p) => p.name)).toContain(persona);
}
for (const area of audit.improvementAreas) {
expect(area.impact).toBeGreaterThan(0);
expect(area.recommendation.length).toBeGreaterThan(10);
}
```
If TypeScript flags `await` as unnecessary for `testInfo.outputPath`, remove `await` and keep the same assertions.
- [ ] **Step 3: Run the spec**
Run:
```bash
node node_modules/playwright/cli.js test tests/e2e/ai-os-positioning-audit.spec.ts --project=chromium --reporter=list
```
Expected: PASS.
---
### Task 6: Verification Sweep
**Files:**
- Verify: `tests/e2e/ai-os-positioning-audit.spec.ts`
- [ ] **Step 1: Run focused Playwright audit**
Run:
```bash
node node_modules/playwright/cli.js test tests/e2e/ai-os-positioning-audit.spec.ts --project=chromium --reporter=list
```
Expected: PASS.
- [ ] **Step 2: Run TypeScript syntax check via Playwright transpilation**
Run:
```bash
node node_modules/playwright/cli.js test tests/e2e/ai-os-positioning-audit.spec.ts --project=chromium --reporter=list --list
```
Expected: test is listed without TypeScript parse errors.
- [ ] **Step 3: Inspect generated report**
Open the newest Playwright output attachment or test result directory and verify the Markdown contains:
```text
# Waggle AI OS Positioning Audit
Overall score:
AI OS verdict:
Addiction level:
## Persona Scores
## Improvement Areas
Sofia
Mara
Imran
Daniel
Priya
```
- [ ] **Step 4: Check git diff**
Run:
```bash
git diff -- tests/e2e/ai-os-positioning-audit.spec.ts
git status --short
```
Expected: only `tests/e2e/ai-os-positioning-audit.spec.ts` is modified/added for implementation, plus any existing unrelated untracked files remain untouched.
---
### Task 7: Commit Implementation
**Files:**
- Stage: `tests/e2e/ai-os-positioning-audit.spec.ts`
- [ ] **Step 1: Stage only the audit spec**
Run:
```bash
git add -- tests/e2e/ai-os-positioning-audit.spec.ts
git status --short
```
Expected: the new audit spec is staged. Existing unrelated untracked files remain unstaged.
- [ ] **Step 2: Commit**
Run:
```bash
git commit -m "test: add ai os positioning audit"
```
Expected: commit succeeds.
- [ ] **Step 3: Final status**
Run:
```bash
git status --short
```
Expected: no staged changes. The pre-existing untracked brief may still appear and must not be touched.