Skip to content

Commit d7d917a

Browse files
Hardening v2.7claude
andcommitted
chore: track two regression tests, bump natively-api, ignore root dispute docs
Sweeps up what was left untracked in the working directory, with one deliberate exclusion. gitignore — the reason this is not a routine chore: KAUSHAL_12_DISPUTE_HANDOVER.md sits at the repo ROOT, a byte-identical stray copy of the file already held in the correctly-ignored dispute-evidence-kaushal-shivaprakashan/. The existing rules are all directory-scoped (/dispute-evidence-*), so the root copy was fully committable and the next `git add -A` would have published it. This repo is PUBLIC and that document carries a named customer's email address, phone number and home address. Adds /*_DISPUTE_HANDOVER.md and /*_DISPUTE_*.md so the root copies can never be staged. README.md and other root docs stay committable — the rule is scoped, not a blanket. Tests (new, previously untracked alongside 279 tracked siblings): V3SkillInjection2026_08_05 — PR Natively-AI-assistant#429 Bug 003, skill injection silently dropped when Context Intelligence V3 is active (default ON since 2026-07-30), covering both drop sites independently. ManualScreenshotScreenContext2026_08_05 — manual screenshot screen context. natively-api: cecbfdf → bedc55e, which is merged to that repo's main via PR Natively-AI-assistant#7 and is the deployed revision (Railway reports Online, /health 200). transitions/: 27 CSS transition reference docs. Scanned for credentials and customer data before publishing — the single hit is the word "password" inside a prose description of form-validation UX. These are tooling reference material rather than application source; say the word and they come back out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SiDsWPD5XtBYgXWt7ZPizu
1 parent 559e0a9 commit d7d917a

31 files changed

Lines changed: 3368 additions & 1 deletion

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,12 @@ user log/
418418
/dispute_evidence/
419419
scripts/*evidence*.py
420420
scripts/combine_*_single_file.py
421+
# The dispute handover docs are ALSO written to the repo root, outside every
422+
# directory rule above — byte-identical strays that a `git add -A` would sweep
423+
# in. This repo is PUBLIC and those files carry a named customer's email, phone
424+
# number and home address, so the root copies need their own rule.
425+
/*_DISPUTE_HANDOVER.md
426+
/*_DISPUTE_*.md
421427
# Autopilot campaign prompts — internal docs, not source
422428
/HANDOFF*.md
423429
/NATIVELY_CONTEXT_SYSTEM_SIMPLIFICATION_PROMPT.md
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Regression test for PR #429 Bug 002: "screenshot attached but code not
2+
// generated" — a manually-attached screenshot never set hasScreenContext, so
3+
// the V3 turn classifier never added SCREEN_SPECIFIC / SCREEN_FACT and the
4+
// screen was not treated as authoritative evidence.
5+
//
6+
// Two drop sites, one per surface:
7+
//
8+
// 1. WTA overlay (IntelligenceEngine.ts, buildV3Prompt input):
9+
// `hasScreenContext: Boolean(options?.screenContext)` — options.screenContext
10+
// is the periodic-capture OCR object; a manually-attached screenshot rides
11+
// in `imagePaths` with screenContext null, so the flag was always false.
12+
//
13+
// 2. Manual chat (ipcHandlers.ts, gemini-chat-stream buildV3Prompt call):
14+
// hasScreenContext was omitted entirely, defaulting to undefined/false
15+
// even when the user attached screenshots.
16+
17+
import { test, describe } from 'node:test';
18+
import assert from 'node:assert/strict';
19+
import fs from 'node:fs';
20+
import path from 'node:path';
21+
import { fileURLToPath } from 'node:url';
22+
23+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
24+
const root = path.resolve(__dirname, '../../..');
25+
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
26+
27+
describe('Bug 002: manually attached screenshots set hasScreenContext', () => {
28+
test('WTA surface: hasScreenContext covers the imagePaths channel, not just periodic OCR', () => {
29+
const source = read('electron/IntelligenceEngine.ts');
30+
assert.doesNotMatch(source, /hasScreenContext: Boolean\(options\?\.screenContext\),/,
31+
'the OCR-only guard must be widened to cover imagePaths');
32+
assert.match(source, /hasScreenContext: Boolean\(options\?\.screenContext\) \|\| \(imagePaths\?\.length \?\? 0\) > 0,/,
33+
'hasScreenContext must be true when manual screenshots ride in imagePaths');
34+
});
35+
36+
test('manual-chat surface: the gemini-chat-stream buildV3Prompt call passes hasScreenContext from imagePaths', () => {
37+
const source = read('electron/ipcHandlers.ts');
38+
// Scope the assertion to the manual-chat V3 composition block.
39+
const start = source.indexOf("surface: 'manual-chat'");
40+
assert.ok(start !== -1, 'manual-chat V3 composition must exist');
41+
const block = source.slice(start, start + 4000);
42+
assert.match(block, /hasScreenContext: \(imagePaths\?\.length \?\? 0\) > 0,/,
43+
'the manual-chat buildV3Prompt call must derive hasScreenContext from attached imagePaths');
44+
});
45+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Regression test for PR #429 Bug 003: skill injection silently ignored when
2+
// Context Intelligence V3 is active (default ON since 2026-07-30).
3+
//
4+
// Two independent drop sites, one per surface:
5+
//
6+
// 1. WTA overlay (WhatToAnswerLLM.ts): the system prompt was composed as
7+
// `_v3p?.system ?? finalPromptOverride`. With V3 active `_v3p` is always
8+
// defined, so `finalPromptOverride` — the only carrier of the
9+
// `## ACTIVE SKILL` block — was silently discarded.
10+
//
11+
// 2. Manual chat (ipcHandlers.ts): the V3 short-circuit owns the turn end to
12+
// end and consumed only the raw `message`, but the /skill-name prefix
13+
// parsing lived ~500 lines later on the legacy path. Under V3 the prefix
14+
// leaked to the model as literal text and the skill instructions were
15+
// never injected anywhere.
16+
17+
import { test, describe } from 'node:test';
18+
import assert from 'node:assert/strict';
19+
import fs from 'node:fs';
20+
import path from 'node:path';
21+
import { fileURLToPath } from 'node:url';
22+
import { composeWtaSystemPrompt } from '../../../dist-electron/electron/llm/index.js';
23+
24+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
25+
const root = path.resolve(__dirname, '../../..');
26+
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
27+
28+
// ---------------------------------------------------------------------------
29+
// 1. WTA surface — behavioral contract of the system-prompt composition
30+
// ---------------------------------------------------------------------------
31+
describe('composeWtaSystemPrompt (WTA surface)', () => {
32+
const skill = { id: 'humanize-ai-text', name: 'Humanize AI Text', promptBlock: '<active_skill>rewrite naturally</active_skill>' };
33+
34+
test('V3 system + active skill → skill block is appended, V3 system preserved', () => {
35+
const out = composeWtaSystemPrompt('V3 SYSTEM PROMPT', 'LEGACY OVERRIDE', skill);
36+
assert.ok(out.startsWith('V3 SYSTEM PROMPT'), 'V3 system prompt must lead');
37+
assert.ok(out.includes(skill.promptBlock), 'skill promptBlock must be present');
38+
});
39+
40+
test('V3 system + no skill → exactly the V3 system prompt (no behavior change)', () => {
41+
assert.equal(composeWtaSystemPrompt('V3 SYSTEM PROMPT', 'LEGACY OVERRIDE', undefined), 'V3 SYSTEM PROMPT');
42+
assert.equal(composeWtaSystemPrompt('V3 SYSTEM PROMPT', 'LEGACY OVERRIDE', null), 'V3 SYSTEM PROMPT');
43+
});
44+
45+
test('no V3 prompt → legacy override verbatim, with or without skill', () => {
46+
assert.equal(composeWtaSystemPrompt(undefined, 'LEGACY OVERRIDE', skill), 'LEGACY OVERRIDE');
47+
assert.equal(composeWtaSystemPrompt(null, 'LEGACY OVERRIDE', undefined), 'LEGACY OVERRIDE');
48+
});
49+
50+
test('WhatToAnswerLLM wires the composition helper at the _wtaSystemPrompt site', () => {
51+
const source = read('electron/llm/WhatToAnswerLLM.ts');
52+
assert.match(source, /_wtaSystemPrompt = composeWtaSystemPrompt\(/,
53+
'_wtaSystemPrompt must be built via composeWtaSystemPrompt so activeSkill survives V3');
54+
assert.doesNotMatch(source, /_wtaSystemPrompt = _v3p\?\.system \?\? finalPromptOverride/,
55+
'the raw ??-discard pattern must be gone');
56+
});
57+
});
58+
59+
// ---------------------------------------------------------------------------
60+
// 2. Manual-chat surface — skill parse must precede the V3 short-circuit and
61+
// the composed V3 system prompt must carry the skill block
62+
// ---------------------------------------------------------------------------
63+
describe('manual-chat V3 short-circuit honors /skill prefixes', () => {
64+
test('skill prefix parsing occurs BEFORE the V3 short-circuit', () => {
65+
const source = read('electron/ipcHandlers.ts');
66+
const skillParseIdx = source.indexOf('skillPrefixMatch');
67+
const v3EntryIdx = source.indexOf('isContextIntelligenceV3Enabled()');
68+
assert.ok(skillParseIdx !== -1 && v3EntryIdx !== -1, 'both sites must exist');
69+
assert.ok(skillParseIdx < v3EntryIdx,
70+
'skill prefix must be parsed before the V3 branch so V3 sees the stripped query and the skill block');
71+
});
72+
73+
test('V3 stream sends a system prompt that includes skillPromptBlock when set', () => {
74+
const source = read('electron/ipcHandlers.ts');
75+
assert.match(source, /skillPromptBlock\s*\?\s*`\$\{composed\.system\}[\s\S]{0,40}\$\{skillPromptBlock\}`/,
76+
'the V3 system prompt must append skillPromptBlock when a skill is active');
77+
});
78+
79+
test('legacy context injection site still exists (non-V3 path unchanged)', () => {
80+
const source = read('electron/ipcHandlers.ts');
81+
assert.match(source, /context = context \? `\$\{skillPromptBlock\}\\n\\n\$\{context\}` : skillPromptBlock/,
82+
'legacy skill-into-context injection must remain for the non-V3 path');
83+
});
84+
85+
// Code-review follow-up (2026-08-05): hoisting the parse above V3 means the
86+
// legacy identity-probe now sees the STRIPPED message. Pre-hoist, the probe
87+
// ran on the raw "/humanize who are you", which its fully-anchored regexes
88+
// (manualIdentityRouting.ts ^...$) could never match — skill turns were
89+
// structurally immune to probe hijacking. The probe must therefore be
90+
// skipped whenever a skill is active, or "/humanize who are you" on the
91+
// legacy path answers with the canned identity reply and drops the skill.
92+
test('legacy identity probe is skipped when a skill is active', () => {
93+
const source = read('electron/ipcHandlers.ts');
94+
assert.match(source, /if \(!skillPromptBlock && !imagePaths\?\.length && typeof message === 'string'\) \{/,
95+
'the identity-probe gate must include !skillPromptBlock so skill turns bypass the probe');
96+
});
97+
});

natively-api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit cecbfdf531208fbcf373cd5b09455a57dd4f0473
1+
Subproject commit bedc55e26a5528258bfd760f6005fdb88a95f699

transitions/accordion.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Accordion expand
2+
3+
## When to use
4+
5+
A disclosure / accordion / collapsible section whose panel grows and shrinks in height when toggled, with the header chevron flipping between a downward "v" and an upward "^". Use for settings groups, FAQs, filter sections, "show more" details — any header + collapsible body.
6+
7+
Height animates via `grid-template-rows: 0fr ↔ 1fr`, so there's **no JS height measuring** and content of any size animates cleanly. The chevron flips vertically (`scaleY`) from a "v" to a "^", passing through a flat line at the midpoint.
8+
9+
## HTML usage
10+
11+
```html
12+
<div class="t-acc" data-open="false">
13+
<button class="t-acc-head" aria-expanded="false">
14+
Title
15+
<span class="t-acc-chevron">
16+
<svg viewBox="0 0 16 16"><path d="M4 6.5L8 10.5L12 6.5"/></svg>
17+
</span>
18+
</button>
19+
<div class="t-acc-panel"><div class="t-acc-panel-inner"> … </div></div>
20+
</div>
21+
```
22+
23+
Toggle `data-open` on the item. The panel animates via
24+
grid-template-rows 0fr ↔ 1fr (no JS height measuring) and
25+
the chevron flips vertically (scaleY) from a "v" to a "^".
26+
27+
## Tunable variables
28+
29+
| Variable | Default | Notes |
30+
| --- | --- | --- |
31+
| `--acc-expand` | `250ms` | sourced from `--p21-expand-dur` |
32+
| `--acc-collapse` | `250ms` | sourced from `--p21-collapse-dur` |
33+
| `--acc-chevron` | `250ms` | sourced from `--p21-chevron-dur` |
34+
| `--acc-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p21-ease` |
35+
36+
The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
37+
38+
```css
39+
:root {
40+
--acc-expand: 250ms;
41+
--acc-collapse: 250ms;
42+
--acc-chevron: 250ms;
43+
--acc-ease: cubic-bezier(0.22, 1, 0.36, 1);
44+
}
45+
```
46+
47+
## CSS
48+
49+
```css
50+
/* grid-template-rows 0fr → 1fr gives a clean height animation
51+
with no JS measurement; the inner element clips overflow. */
52+
.t-acc-panel {
53+
display: grid;
54+
grid-template-rows: 0fr;
55+
transition: grid-template-rows var(--acc-collapse) var(--acc-ease);
56+
}
57+
.t-acc[data-open="true"] .t-acc-panel {
58+
grid-template-rows: 1fr;
59+
transition: grid-template-rows var(--acc-expand) var(--acc-ease);
60+
}
61+
.t-acc-panel-inner {
62+
overflow: hidden;
63+
opacity: 0;
64+
filter: blur(2px);
65+
transition:
66+
opacity var(--acc-collapse) var(--acc-ease),
67+
filter var(--acc-collapse) var(--acc-ease);
68+
}
69+
.t-acc[data-open="true"] .t-acc-panel-inner {
70+
opacity: 1;
71+
filter: blur(0);
72+
transition:
73+
opacity var(--acc-expand) var(--acc-ease),
74+
filter var(--acc-expand) var(--acc-ease);
75+
}
76+
/* Flip the chevron vertically to turn the "v" into a "^".
77+
scaleY(-1) about the centre passes through a flat line at
78+
the midpoint (same look as a `d:` path morph) but animates
79+
in every browser, unlike CSS `d:` morphing (Chromium only).
80+
The chevron path is symmetric about the 16x16 viewBox
81+
centre, so the flip lands exactly on the "^"; non-scaling
82+
-stroke keeps the stroke width constant through the flip. */
83+
.t-acc-chevron {
84+
display: inline-flex;
85+
transform: scaleY(1);
86+
transform-origin: center;
87+
transition: transform var(--acc-chevron) var(--acc-ease);
88+
}
89+
.t-acc-chevron path { vector-effect: non-scaling-stroke; }
90+
.t-acc[data-open="true"] .t-acc-chevron {
91+
transform: scaleY(-1);
92+
}
93+
94+
@media (prefers-reduced-motion: reduce) {
95+
.t-acc-panel, .t-acc-panel-inner, .t-acc-chevron {
96+
transition: none !important;
97+
}
98+
}
99+
```
100+
101+
The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
102+
103+
## JavaScript orchestration
104+
105+
```js
106+
// Toggle data-open on the item; CSS owns the height + chevron morph.
107+
const acc = document.querySelector(".t-acc");
108+
const head = acc.querySelector(".t-acc-head");
109+
110+
head.addEventListener("click", () => {
111+
const open = acc.getAttribute("data-open") === "true";
112+
acc.setAttribute("data-open", String(!open));
113+
head.setAttribute("aria-expanded", String(!open));
114+
});
115+
```
116+
117+
### Two-element panel + padding placement
118+
119+
The panel needs the two-element structure (`.t-acc-panel` grid track + `.t-acc-panel-inner` with `overflow: hidden`). The `0fr → 1fr` track can only collapse a child that clips its own overflow. Keep padding on `.t-acc-panel-inner`, never on `.t-acc-panel` — padding on the `0fr` track leaves a residual height strip so the panel never fully closes.
120+
121+
### Why the chevron flips instead of morphing its path
122+
123+
The natural way to turn the "v" into a "^" is to morph the chevron's SVG `d` between two vertex sets — but CSS `d:` path interpolation is **Chromium-only**, so on mobile Safari and Firefox it snaps (or doesn't move at all). A vertical flip (`transform: scaleY(-1)`) reproduces the same motion — it passes through a flat horizontal line at the midpoint, exactly like the path morph — and animates in every browser. Two requirements make it land cleanly: the chevron path must be **symmetric about the centre of its viewBox** (so the flip maps the "v" onto the "^"), and the path needs `vector-effect: non-scaling-stroke` so the stroke width stays constant while the box is squashed mid-flip.
124+

0 commit comments

Comments
 (0)