create-principles-disciple 1.145.41 → 1.145.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release/product-identity.json +1 -1
- package/codex-adapter/package.json +1 -1
- package/console/dist/server/index.js +8 -0
- package/console/dist/server/routes/examiner-executor.d.ts +254 -0
- package/console/dist/server/routes/examiner-executor.js +622 -0
- package/console/dist/server/routes/examiner.d.ts +46 -0
- package/console/dist/server/routes/examiner.js +296 -0
- package/console/dist/ui/App.js +6 -3
- package/console/dist/ui/api.d.ts +138 -0
- package/console/dist/ui/api.js +56 -0
- package/console/dist/ui/components/layout/app-sidebar.d.ts +11 -0
- package/console/dist/ui/components/layout/app-sidebar.js +12 -2
- package/console/dist/ui/i18n/en.json +63 -0
- package/console/dist/ui/i18n/zh-CN.json +63 -0
- package/console/dist/ui/pages/examiner/ExaminerPage.d.ts +15 -0
- package/console/dist/ui/pages/examiner/ExaminerPage.js +158 -0
- package/console/dist/ui/pages/principles/PrincipleDetailPage.js +2 -1
- package/console/dist/ui/utils/runtime-mode.d.ts +10 -0
- package/console/dist/ui/utils/runtime-mode.js +10 -0
- package/console/dist/web/assets/app.css +44 -0
- package/console/dist/web/assets/app.js +33 -31
- package/core/dist/host/host-installer.d.ts +13 -0
- package/core/dist/host/host-installer.d.ts.map +1 -1
- package/core/dist/host/host-installer.js.map +1 -1
- package/core/package.json +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -1
- package/dist/installer.js +1 -1
- package/dist/installer.js.map +1 -1
- package/dist/installers/workbuddy-host-installer.d.ts +39 -0
- package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/dist/installers/workbuddy-host-installer.js +76 -27
- package/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/package.json +2 -2
- package/pd-cli/dist/services/pd-config-loader.d.ts +1 -1
- package/pd-cli/dist/services/pd-config-loader.d.ts.map +1 -1
- package/pd-cli/dist/services/pd-config-loader.js +9 -0
- package/pd-cli/dist/services/pd-config-loader.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/dist/index.d.ts.map +1 -1
- package/release-manager/dist/index.js +29 -0
- package/release-manager/dist/index.js.map +1 -1
- package/release-manager/dist/installer.js +1 -1
- package/release-manager/dist/installer.js.map +1 -1
- package/release-manager/dist/installers/workbuddy-host-installer.d.ts +39 -0
- package/release-manager/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/release-manager/dist/installers/workbuddy-host-installer.js +76 -27
- package/release-manager/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/release-manager/package.json +2 -2
- package/workbuddy-adapter/package.json +1 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { sendSuccess, sendError, sendNotFound } from '../utils/response.js';
|
|
4
|
+
import { PrinciplesConsoleModel } from '../models/PrinciplesConsoleModel.js';
|
|
5
|
+
import { loadLabModules, loadHostCapability, preflightCheck, offlineStudyPass, reportMaterialize, listStudyNames, resolveStudyAccess, resolveStudyRoots, studyStatusPayload, studyReportPayload, materializeConsoleStudy, verifyConsoleOwnership, writableRootProblem, isJsonObject, ExaminerPreflightBlockedError, } from './examiner-executor.js';
|
|
6
|
+
/* eslint-disable @typescript-eslint/max-params -- node:http handler signature (req, res, workspaceDir, subPath) matches every other route */
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const repoRoot = path.resolve(here, '..', '..', '..', '..', '..');
|
|
9
|
+
const labRoot = path.join(repoRoot, 'scripts', 'dev', 'principle-learning-lab');
|
|
10
|
+
const PRINCIPLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
11
|
+
const STUDY_ROUTE_PATTERN = /^\/studies\/([A-Za-z0-9-]+)(\/[^/]*)?$/;
|
|
12
|
+
/**
|
|
13
|
+
* P2-1: the Examiner is a DEV-ONLY capability. The gate is SERVER-SIDE and
|
|
14
|
+
* cannot be set by a client — it reads only the running module's own URL and
|
|
15
|
+
* the operator's environment: a source run (tsx/vitest, import.meta.url ends
|
|
16
|
+
* with .ts) or an explicit `PD_EXAMINER_DEV=1`. `NODE_ENV=production` is an
|
|
17
|
+
* UNCONDITIONAL hard gate: not even the DEV override can enable the surface
|
|
18
|
+
* there, so a production build (`dist/server.js`) answers 404 — never 503.
|
|
19
|
+
*/
|
|
20
|
+
export function isExaminerDevRuntime(moduleUrl, env = process.env) {
|
|
21
|
+
if (env.NODE_ENV === 'production')
|
|
22
|
+
return false;
|
|
23
|
+
if (env.PD_EXAMINER_DEV === '1')
|
|
24
|
+
return true;
|
|
25
|
+
return moduleUrl.endsWith('.ts');
|
|
26
|
+
}
|
|
27
|
+
function examinerDevEnabled() {
|
|
28
|
+
return isExaminerDevRuntime(import.meta.url);
|
|
29
|
+
}
|
|
30
|
+
/** The runner's lock-time re-verification refusal (dynamic module: match the
|
|
31
|
+
* contract name, not an instanceof across import boundaries). */
|
|
32
|
+
function isFrozenStudyDriftError(error) {
|
|
33
|
+
return error instanceof Error && error.name === 'FrozenStudyDriftError';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Drift details are CONTRACT output (check names + case ids), but they travel
|
|
37
|
+
* into an HTTP payload: scrub anything path-shaped and bound the size so a
|
|
38
|
+
* refusal never leaks filesystem internals or parser text.
|
|
39
|
+
*/
|
|
40
|
+
function driftProblems(details) {
|
|
41
|
+
if (typeof details !== 'string')
|
|
42
|
+
return [];
|
|
43
|
+
return details
|
|
44
|
+
.split(';')
|
|
45
|
+
.map((part) => part.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((part) => part
|
|
48
|
+
.replace(/[A-Za-z]:[\\/][^\s;]*/g, '<path>')
|
|
49
|
+
.replace(/(^|[\s(=])\/[^\s;()]+/g, '$1<path>')
|
|
50
|
+
.slice(0, 240))
|
|
51
|
+
.slice(0, 20);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Responses carry stable reason codes, never raw exception text: a thrown
|
|
55
|
+
* message can embed absolute paths and other internals that are not
|
|
56
|
+
* Owner-facing information. The full error stays in the server log.
|
|
57
|
+
*/
|
|
58
|
+
function logRefusal(scope, error) {
|
|
59
|
+
console.error(`[pd-console] examiner ${scope}:`, error);
|
|
60
|
+
}
|
|
61
|
+
let labModules = null;
|
|
62
|
+
let hostCapability = null;
|
|
63
|
+
let labUnavailableReason = null;
|
|
64
|
+
async function loadLab() {
|
|
65
|
+
if (hostCapability)
|
|
66
|
+
return hostCapability;
|
|
67
|
+
try {
|
|
68
|
+
labModules = await loadLabModules(labRoot);
|
|
69
|
+
hostCapability = await loadHostCapability(labRoot);
|
|
70
|
+
return hostCapability;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logRefusal('lab unavailable', error);
|
|
74
|
+
labUnavailableReason = 'lab tooling could not be loaded (details in the console server log)';
|
|
75
|
+
labModules = null;
|
|
76
|
+
hostCapability = null;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function readBody(req) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
let raw = '';
|
|
83
|
+
req.on('data', (chunk) => {
|
|
84
|
+
raw += String(chunk);
|
|
85
|
+
});
|
|
86
|
+
req.on('end', () => {
|
|
87
|
+
if (!raw) {
|
|
88
|
+
resolve({});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
resolve(isJsonObject(parsed) ? parsed : {});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
resolve({});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export async function handleExaminerRoute(req, res, workspaceDir, subPath) {
|
|
102
|
+
if (!examinerDevEnabled()) {
|
|
103
|
+
sendError(res, 404, 'examiner_not_enabled', 'The Examiner research surface is a local-dev capability and is not enabled in this runtime');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const lab = await loadLab();
|
|
107
|
+
if (!lab || !labModules) {
|
|
108
|
+
sendError(res, 503, 'examiner_dev_tooling_unavailable', `Examiner dev tooling is not available in this installation (the principle-learning-lab lives in the repository's scripts/dev area): ${labUnavailableReason ?? 'unknown reason'}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const method = req.method ?? 'GET';
|
|
112
|
+
const roots = resolveStudyRoots(labRoot);
|
|
113
|
+
const context = { labRoot, roots };
|
|
114
|
+
const rootProblem = writableRootProblem(roots);
|
|
115
|
+
// GET /overview
|
|
116
|
+
if (subPath === '/overview' && method === 'GET') {
|
|
117
|
+
const writable = listStudyNames(roots.writable);
|
|
118
|
+
const historical = listStudyNames(roots.historical);
|
|
119
|
+
sendSuccess(res, {
|
|
120
|
+
spec: 'SPEC-PD-EXAMINER-001 v0.9',
|
|
121
|
+
executionScope: 'offline-stub only — real model calls and real host runs are NOT authorized this round',
|
|
122
|
+
gates: {
|
|
123
|
+
gate1_challenge_trust: 'BLOCKED — independent human Gold review has not been performed for any Examiner suite (code tests can never substitute)',
|
|
124
|
+
gate2_behavior_evaluation: 'NOT_READY — requires Gate 1 + real-host authorization',
|
|
125
|
+
gate3_llm_authoring: 'NOT_STARTED',
|
|
126
|
+
gate4_product_integration: 'NOT_STARTED (local-dev console integration; the production UI/API is disabled)',
|
|
127
|
+
},
|
|
128
|
+
hostInjectionCapability: lab,
|
|
129
|
+
fullConclusionMinimumLevel: 'CONTEXT_PRESENT',
|
|
130
|
+
writableStudiesRoot: roots.writable,
|
|
131
|
+
historicalStudiesRoot: roots.historical,
|
|
132
|
+
rootProblem,
|
|
133
|
+
studies: [...writable, ...historical.filter((name) => !writable.includes(name))],
|
|
134
|
+
readOnlyStudies: [
|
|
135
|
+
...historical,
|
|
136
|
+
...writable.filter((name) => !verifyConsoleOwnership(roots.writable, path.join(roots.writable, name)).ok),
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// GET /principles — real workspace principles as study inputs (read-only)
|
|
142
|
+
if (subPath === '/principles' && method === 'GET') {
|
|
143
|
+
const model = new PrinciplesConsoleModel(workspaceDir);
|
|
144
|
+
const list = await model.listPrinciples();
|
|
145
|
+
const principles = (list.principles ?? []).map((p) => ({ id: p.id, version: 'ledger', text: p.text, status: p.status, ruleCount: p.ruleCount ?? 0 }));
|
|
146
|
+
sendSuccess(res, { principles, note: 'study inputs are read from the workspace ledger (read-only); eligibility filtering happens inside the Examiner contract' });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// POST /studies — create an offline examiner study (identity server-minted)
|
|
150
|
+
if (subPath === '/studies' && method === 'POST') {
|
|
151
|
+
if (rootProblem) {
|
|
152
|
+
sendError(res, 409, 'writable_root_not_isolated', rootProblem);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const body = await readBody(req);
|
|
156
|
+
const requestedTarget = typeof body.targetPrincipleId === 'string' ? body.targetPrincipleId.trim() : '';
|
|
157
|
+
const requestedMode = typeof body.experimentMode === 'string' ? body.experimentMode : '';
|
|
158
|
+
const repetitionsRaw = Number(body.repetitionsPerCase);
|
|
159
|
+
const repetitionsPerCase = Number.isInteger(repetitionsRaw) && repetitionsRaw >= 1 && repetitionsRaw <= 5 ? repetitionsRaw : 2;
|
|
160
|
+
if (!PRINCIPLE_ID_PATTERN.test(requestedTarget)) {
|
|
161
|
+
sendError(res, 400, 'invalid_target_principle', 'targetPrincipleId required (charset-validated principle id; one target Principle per study)');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const model = new PrinciplesConsoleModel(workspaceDir);
|
|
165
|
+
const list = await model.listPrinciples();
|
|
166
|
+
const ledgerPrinciples = (list.principles ?? []).map((p) => ({ id: p.id, version: 'ledger', text: p.text, status: p.status, ruleCount: p.ruleCount ?? 0 }));
|
|
167
|
+
// The request only SELECTS: the target travels onward as an INDEX into
|
|
168
|
+
// the server's own ledger list (exact-match required) and the mode as a
|
|
169
|
+
// BOOLEAN; the study identity/directory/seed are minted server-side. No
|
|
170
|
+
// request string crosses into the research pipeline.
|
|
171
|
+
const targetIndex = ledgerPrinciples.findIndex((p) => p.id === requestedTarget);
|
|
172
|
+
if (targetIndex < 0) {
|
|
173
|
+
sendError(res, 404, 'target_principle_not_found', 'the target principle was not found in the workspace ledger');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const isolatedMode = requestedMode === 'isolated_principle';
|
|
177
|
+
try {
|
|
178
|
+
const created = await materializeConsoleStudy(labModules, labRoot, roots, {
|
|
179
|
+
isolatedMode,
|
|
180
|
+
targetIndex,
|
|
181
|
+
ledgerPrinciples,
|
|
182
|
+
repetitionsPerCase,
|
|
183
|
+
});
|
|
184
|
+
sendSuccess(res, {
|
|
185
|
+
studyId: created.studyId,
|
|
186
|
+
targetPrincipleId: created.targetPrincipleId,
|
|
187
|
+
experimentMode: isolatedMode ? 'isolated_principle' : 'full_library_incremental',
|
|
188
|
+
plannedPairs: created.plannedPairs,
|
|
189
|
+
manifestHash: created.manifestHash,
|
|
190
|
+
note: 'offline study created with SYNTHETIC gold — run preflight, then the offline-stub execution; results demonstrate the contract pipeline, never real host behavior',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
logRefusal('study creation refused', error);
|
|
195
|
+
sendError(res, 422, 'study_creation_refused', 'the study was refused before any file was written (details in the console server log)');
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// /studies/:studyId[/...]
|
|
200
|
+
const studyMatch = STUDY_ROUTE_PATTERN.exec(subPath);
|
|
201
|
+
if (studyMatch && studyMatch[1]) {
|
|
202
|
+
const rest = studyMatch[2] ?? '';
|
|
203
|
+
const access = resolveStudyAccess(roots, studyMatch[1]);
|
|
204
|
+
if (!access) {
|
|
205
|
+
sendError(res, 404, 'study_not_found', 'study not found');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const studyDir = access.dir;
|
|
209
|
+
// GET status
|
|
210
|
+
if (rest === '' && method === 'GET') {
|
|
211
|
+
sendSuccess(res, { ...studyStatusPayload(studyDir), readOnly: access.readOnly, access: access.reason });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// POST /preflight — the fail-closed gate over the frozen study facts
|
|
215
|
+
if (rest === '/preflight' && method === 'POST') {
|
|
216
|
+
const preflight = preflightCheck(labModules, studyDir, context);
|
|
217
|
+
sendSuccess(res, { studyId: access.studyId, ok: preflight.ok, status: preflight.status, blockers: preflight.blockers, warnings: preflight.warnings, checks: preflight.checks });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// POST /run — offline-stub only; real modes are refused at THIS boundary.
|
|
221
|
+
// P1-1/P1-4: the study must be server-owned and writable, and the SERVER
|
|
222
|
+
// preflight must pass BEFORE the runner is entered. A blocker returns 409
|
|
223
|
+
// with no RUN_STARTED event, no reservation and no actor creation.
|
|
224
|
+
if (rest === '/run' && method === 'POST') {
|
|
225
|
+
const body = await readBody(req);
|
|
226
|
+
// Real execution needs an Owner-approved run-authorization.json plus the
|
|
227
|
+
// Stage R gate — this console route NEVER transports a caller-supplied
|
|
228
|
+
// mode into the runner; only the offline-stub default is reachable.
|
|
229
|
+
if (body.mode !== undefined && body.mode !== 'offline-stub') {
|
|
230
|
+
sendError(res, 403, 'real_execution_not_authorized', 'non-offline execution requires explicit Owner authorization (run-authorization.json + Stage R gate) — NOT GRANTED this round');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (access.readOnly) {
|
|
234
|
+
sendError(res, 403, 'study_read_only', `this study cannot be executed from the console: ${access.reason}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const summary = await offlineStudyPass(labModules, studyDir, context);
|
|
239
|
+
sendSuccess(res, { studyId: access.studyId, units: summary.units.map((unit) => ({ unit: unit.unit, state: unit.state })) });
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
// P1-4: both gates answer the same 409 shape. The server preflight
|
|
243
|
+
// refuses BEFORE RUN_STARTED; the runner re-verifies UNDER the study
|
|
244
|
+
// lock and refuses on drift discovered after that first check. Either
|
|
245
|
+
// way: no RUN_STARTED, no reservation, no actor, no attempt file.
|
|
246
|
+
if (error instanceof ExaminerPreflightBlockedError) {
|
|
247
|
+
sendError(res, 409, 'preflight_blocked', 'offline preflight blocked; no run started, no unit reserved, no actor created', {
|
|
248
|
+
blockers: error.blockers,
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (isFrozenStudyDriftError(error)) {
|
|
253
|
+
sendError(res, 409, 'preflight_blocked', 'the frozen study drifted after preflight; no run started, no unit reserved, no actor created', {
|
|
254
|
+
blockers: [{ id: 'frozen_study_drift', problems: driftProblems(error.details) }],
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
logRefusal('offline run failed', error);
|
|
259
|
+
sendError(res, 500, 'offline_run_failed', 'the offline run failed (details in the console server log)');
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// GET /report — read-only (historical studies stay readable)
|
|
264
|
+
if (rest === '/report' && method === 'GET') {
|
|
265
|
+
const payload = studyReportPayload(labModules, studyDir);
|
|
266
|
+
if (!payload) {
|
|
267
|
+
sendError(res, 404, 'report_not_generated', 'no Report V2 yet — run the offline execution first, then generate the report (the console never auto-reruns or rewrites history)');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
sendSuccess(res, { ...payload, readOnly: access.readOnly });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// POST /report — generate (idempotent; refuses to overwrite a READY report)
|
|
274
|
+
if (rest === '/report' && method === 'POST') {
|
|
275
|
+
if (access.readOnly) {
|
|
276
|
+
sendError(res, 403, 'study_read_only', `this study cannot be written from the console: ${access.reason}`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const result = reportMaterialize(labModules, studyDir, new Date().toISOString(), context, () => {
|
|
281
|
+
const check = verifyConsoleOwnership(roots.writable, studyDir);
|
|
282
|
+
return check.ok ? { ok: true } : { ok: false, reason: check.reason };
|
|
283
|
+
});
|
|
284
|
+
sendSuccess(res, { studyId: access.studyId, status: result.status, written: result.written });
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
logRefusal('report write refused', error);
|
|
288
|
+
sendError(res, 409, 'report_write_refused', 'the report was refused and no report file was written (details in the console server log)');
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
sendNotFound(res, `Route /api/v1/examiner/studies/:studyId${rest} not found`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
sendNotFound(res, `Route /api/v1/examiner${subPath} not found`);
|
|
296
|
+
}
|
package/console/dist/ui/App.js
CHANGED
|
@@ -19,6 +19,7 @@ import { CorePrinciplesPage } from "./pages/principles/CorePrinciplesPage.js";
|
|
|
19
19
|
import { PrincipleDetailPage } from "./pages/principles/PrincipleDetailPage.js";
|
|
20
20
|
import { ActivationPage } from "./pages/activation/ActivationPage.js";
|
|
21
21
|
import { DebtPage } from "./pages/debt/DebtPage.js";
|
|
22
|
+
import { ExaminerPage } from "./pages/examiner/ExaminerPage.js";
|
|
22
23
|
import { ControlCenterPage } from "./pages/control-center/ControlCenterPage.js";
|
|
23
24
|
import { SettingsPage } from "./pages/settings/SettingsPage.js";
|
|
24
25
|
import { UpdatePage } from "./pages/settings/UpdatePage.js";
|
|
@@ -28,8 +29,10 @@ import { IntentPage } from "./pages/intent/IntentPage.js";
|
|
|
28
29
|
import { DesignSystemPage } from "./pages/design-system/DesignSystemPage.js";
|
|
29
30
|
import { WelcomePage } from "./pages/welcome/WelcomePage.js";
|
|
30
31
|
import { getOnboardingState } from "./utils/onboarding-state.js";
|
|
31
|
-
// Vite replaces import.meta.env.DEV with a boolean literal at build time
|
|
32
|
-
//
|
|
32
|
+
// Vite replaces import.meta.env.DEV with a boolean literal at build time, so a
|
|
33
|
+
// production bundle resolves this to false and dead-code elimination drops the
|
|
34
|
+
// DEV-only routes from the shipped UI. `utils/runtime-mode.ts` exports the same
|
|
35
|
+
// value for components rendered outside this module; both read the same define.
|
|
33
36
|
const IS_DEV = import.meta.env.DEV;
|
|
34
37
|
/**
|
|
35
38
|
* SPEC-PD-UIUX-001 R4: the skip control must NOT touch the URL hash — the app
|
|
@@ -162,7 +165,7 @@ function AuthRoutes() {
|
|
|
162
165
|
}, []);
|
|
163
166
|
return (_jsxs(Routes, { children: [_jsx(Route, { path: "/splash", element: _jsx(SplashScreen, { onComplete: () => {
|
|
164
167
|
setShowSplash(false);
|
|
165
|
-
} }) }), _jsx(Route, { path: "/login", element: _jsx(LoginForm, { onAuthSuccess: handleAuthSuccess }) }), _jsx(Route, { path: "/*", element: authed === null ? (_jsx("div", { className: "min-h-screen bg-paper flex items-center justify-center", children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("div", { className: "w-8 h-8 border-2 border-gov border-t-transparent rounded-full animate-spin" }), _jsx("span", { className: "text-[12px] font-mono tracking-wider text-ink-3 uppercase", children: "Verifying Session..." })] }) })) : authed ? (_jsx(NotificationProvider, { children: _jsx(AuthenticatedShell, { featureFlags: featureFlags ?? undefined, children: _jsx(ErrorBoundary, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: "/focus", replace: true }) }), _jsx(Route, { path: "/welcome", element: _jsx(WelcomePage, { workspaceId: currentWorkspaceId }) }), _jsx(Route, { path: "/focus", element: _jsx(FocusPage, { featureFlags: featureFlags ?? undefined }) }), _jsx(Route, { path: "/pain", element: _jsx(PainPage, {}) }), _jsx(Route, { path: "/principles", element: _jsx(PrinciplesPage, {}) }), _jsx(Route, { path: "/principles/core", element: _jsx(CorePrinciplesPage, {}) }), _jsx(Route, { path: "/principles/:id", element: _jsx(PrincipleDetailPage, {}) }), _jsx(Route, { path: "/activation", element: _jsx(ActivationPage, {}) }), _jsx(Route, { path: "/debt", element: _jsx(DebtPage, {}) }), _jsx(Route, { path: "/control-center", element: _jsx(ControlCenterPage, {}) }), _jsx(Route, { path: "/settings", element: _jsx(SettingsPage, {}) }), _jsx(Route, { path: "/update", element: _jsx(UpdatePage, {}) }), _jsx(Route, { path: "/report-problem", element: _jsx(ReportProblemPage, {}) }), _jsx(Route, { path: "/failed-tasks", element: _jsx(FailedTasksPage, {}) }), _jsx(Route, { path: "/intent", element: _jsx(IntentPage, {}) }), IS_DEV && (_jsx(Route, { path: "/design-system", element: _jsx(DesignSystemPage, {}) })), !IS_DEV && (_jsx(Route, { path: "/design-system", element: _jsx(Navigate, { to: "/focus", replace: true }) }))] }) }) }) })) : (_jsx(Navigate, { to: "/login", replace: true })) })] }));
|
|
168
|
+
} }) }), _jsx(Route, { path: "/login", element: _jsx(LoginForm, { onAuthSuccess: handleAuthSuccess }) }), _jsx(Route, { path: "/*", element: authed === null ? (_jsx("div", { className: "min-h-screen bg-paper flex items-center justify-center", children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("div", { className: "w-8 h-8 border-2 border-gov border-t-transparent rounded-full animate-spin" }), _jsx("span", { className: "text-[12px] font-mono tracking-wider text-ink-3 uppercase", children: "Verifying Session..." })] }) })) : authed ? (_jsx(NotificationProvider, { children: _jsx(AuthenticatedShell, { featureFlags: featureFlags ?? undefined, children: _jsx(ErrorBoundary, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: "/focus", replace: true }) }), _jsx(Route, { path: "/welcome", element: _jsx(WelcomePage, { workspaceId: currentWorkspaceId }) }), _jsx(Route, { path: "/focus", element: _jsx(FocusPage, { featureFlags: featureFlags ?? undefined }) }), _jsx(Route, { path: "/pain", element: _jsx(PainPage, {}) }), _jsx(Route, { path: "/principles", element: _jsx(PrinciplesPage, {}) }), _jsx(Route, { path: "/principles/core", element: _jsx(CorePrinciplesPage, {}) }), _jsx(Route, { path: "/principles/:id", element: _jsx(PrincipleDetailPage, {}) }), _jsx(Route, { path: "/activation", element: _jsx(ActivationPage, {}) }), _jsx(Route, { path: "/debt", element: _jsx(DebtPage, {}) }), IS_DEV ? (_jsx(Route, { path: "/examiner", element: _jsx(ExaminerPage, {}) })) : (_jsx(Route, { path: "/examiner", element: _jsx(Navigate, { to: "/focus", replace: true }) })), _jsx(Route, { path: "/control-center", element: _jsx(ControlCenterPage, {}) }), _jsx(Route, { path: "/settings", element: _jsx(SettingsPage, {}) }), _jsx(Route, { path: "/update", element: _jsx(UpdatePage, {}) }), _jsx(Route, { path: "/report-problem", element: _jsx(ReportProblemPage, {}) }), _jsx(Route, { path: "/failed-tasks", element: _jsx(FailedTasksPage, {}) }), _jsx(Route, { path: "/intent", element: _jsx(IntentPage, {}) }), IS_DEV && (_jsx(Route, { path: "/design-system", element: _jsx(DesignSystemPage, {}) })), !IS_DEV && (_jsx(Route, { path: "/design-system", element: _jsx(Navigate, { to: "/focus", replace: true }) }))] }) }) }) })) : (_jsx(Navigate, { to: "/login", replace: true })) })] }));
|
|
166
169
|
}
|
|
167
170
|
export function App() {
|
|
168
171
|
return (_jsx(ThemeProvider, { children: _jsxs(I18nextProvider, { i18n: i18n, children: [_jsx(HashRouter, { children: _jsx(AuthRoutes, {}) }), _jsx(Toaster, {})] }) }));
|
package/console/dist/ui/api.d.ts
CHANGED
|
@@ -468,3 +468,141 @@ export interface PrincipleDetailRule {
|
|
|
468
468
|
export interface PrincipleDetailData {
|
|
469
469
|
principle: PrincipleDetail;
|
|
470
470
|
}
|
|
471
|
+
export interface ExaminerOverviewData {
|
|
472
|
+
spec: string;
|
|
473
|
+
executionScope: string;
|
|
474
|
+
gates: Record<string, string>;
|
|
475
|
+
hostInjectionCapability: Record<string, string>;
|
|
476
|
+
fullConclusionMinimumLevel: string;
|
|
477
|
+
studiesRoot: string;
|
|
478
|
+
studies: string[];
|
|
479
|
+
}
|
|
480
|
+
export interface ExaminerPrincipleItem {
|
|
481
|
+
id: string;
|
|
482
|
+
version: string;
|
|
483
|
+
text: string;
|
|
484
|
+
status: string;
|
|
485
|
+
}
|
|
486
|
+
export interface ExaminerStudyStatusData {
|
|
487
|
+
studyId: string;
|
|
488
|
+
experimentMode: string;
|
|
489
|
+
interventionType: string;
|
|
490
|
+
targetPrincipleId: string | null;
|
|
491
|
+
plannedPairs: number | null;
|
|
492
|
+
runStarted: boolean;
|
|
493
|
+
runFinished: boolean;
|
|
494
|
+
unitStates: Record<string, number>;
|
|
495
|
+
reportStatus: string | null;
|
|
496
|
+
}
|
|
497
|
+
export interface ExaminerPreflightCheck {
|
|
498
|
+
id: string;
|
|
499
|
+
severity: string;
|
|
500
|
+
ok: boolean;
|
|
501
|
+
detail: string;
|
|
502
|
+
}
|
|
503
|
+
export interface ExaminerPreflightData {
|
|
504
|
+
studyId: string;
|
|
505
|
+
ok: boolean;
|
|
506
|
+
status: string;
|
|
507
|
+
blockers: ExaminerPreflightCheck[];
|
|
508
|
+
warnings: ExaminerPreflightCheck[];
|
|
509
|
+
checks: ExaminerPreflightCheck[];
|
|
510
|
+
}
|
|
511
|
+
export interface ExaminerReportData {
|
|
512
|
+
studyId: string;
|
|
513
|
+
report: {
|
|
514
|
+
status: string;
|
|
515
|
+
scope?: {
|
|
516
|
+
executionMode?: string;
|
|
517
|
+
controlArm?: string;
|
|
518
|
+
treatmentArm?: string;
|
|
519
|
+
};
|
|
520
|
+
examiner?: {
|
|
521
|
+
experimentMode: string;
|
|
522
|
+
interventionType: string;
|
|
523
|
+
targetPrincipleId: string | null;
|
|
524
|
+
primaryEndpoint: {
|
|
525
|
+
id: string;
|
|
526
|
+
} | null;
|
|
527
|
+
applicability: {
|
|
528
|
+
applicablePairs: number;
|
|
529
|
+
notApplicablePairs: number;
|
|
530
|
+
};
|
|
531
|
+
itt: {
|
|
532
|
+
plannedApplicablePairs: number;
|
|
533
|
+
determinedPairs: number;
|
|
534
|
+
unknownPairs: number;
|
|
535
|
+
cells: Record<string, number>;
|
|
536
|
+
pointEstimate: {
|
|
537
|
+
status: string;
|
|
538
|
+
value?: number;
|
|
539
|
+
numerator?: number;
|
|
540
|
+
denominator?: number;
|
|
541
|
+
};
|
|
542
|
+
lowerBound: {
|
|
543
|
+
status: string;
|
|
544
|
+
value?: number;
|
|
545
|
+
};
|
|
546
|
+
upperBound: {
|
|
547
|
+
status: string;
|
|
548
|
+
value?: number;
|
|
549
|
+
};
|
|
550
|
+
};
|
|
551
|
+
treatmentFidelity: {
|
|
552
|
+
plannedTreatmentUnits: number;
|
|
553
|
+
contextPresent: number;
|
|
554
|
+
runtimeLoaded: number;
|
|
555
|
+
actualInjectionRate: {
|
|
556
|
+
status: string;
|
|
557
|
+
value?: number;
|
|
558
|
+
};
|
|
559
|
+
controlContamination: {
|
|
560
|
+
units: number;
|
|
561
|
+
ofControlChains: number;
|
|
562
|
+
};
|
|
563
|
+
capacityEvictions: {
|
|
564
|
+
units: number;
|
|
565
|
+
};
|
|
566
|
+
};
|
|
567
|
+
randomization: {
|
|
568
|
+
strategy: string;
|
|
569
|
+
verified: boolean;
|
|
570
|
+
} | null;
|
|
571
|
+
} | null;
|
|
572
|
+
dimensions?: Record<string, unknown>;
|
|
573
|
+
limitations?: string[];
|
|
574
|
+
};
|
|
575
|
+
verification: {
|
|
576
|
+
checks: {
|
|
577
|
+
ok: boolean;
|
|
578
|
+
}[];
|
|
579
|
+
};
|
|
580
|
+
note: string;
|
|
581
|
+
}
|
|
582
|
+
export declare function fetchExaminerOverview(): Promise<ApiResponse<ExaminerOverviewData>>;
|
|
583
|
+
export declare function fetchExaminerPrinciples(): Promise<ApiResponse<{
|
|
584
|
+
principles: ExaminerPrincipleItem[];
|
|
585
|
+
}>>;
|
|
586
|
+
export declare function createExaminerStudy(input: {
|
|
587
|
+
targetPrincipleId: string;
|
|
588
|
+
experimentMode: string;
|
|
589
|
+
repetitionsPerCase: number;
|
|
590
|
+
}): Promise<ApiResponse<{
|
|
591
|
+
studyId: string;
|
|
592
|
+
experimentMode: string;
|
|
593
|
+
plannedPairs: number | null;
|
|
594
|
+
note: string;
|
|
595
|
+
}>>;
|
|
596
|
+
export declare function fetchExaminerStudyStatus(studyId: string): Promise<ApiResponse<ExaminerStudyStatusData>>;
|
|
597
|
+
export declare function runExaminerPreflight(studyId: string): Promise<ApiResponse<ExaminerPreflightData>>;
|
|
598
|
+
export declare function runExaminerStudy(studyId: string): Promise<ApiResponse<{
|
|
599
|
+
units: {
|
|
600
|
+
unit: unknown;
|
|
601
|
+
state: string;
|
|
602
|
+
}[];
|
|
603
|
+
}>>;
|
|
604
|
+
export declare function generateExaminerReport(studyId: string): Promise<ApiResponse<{
|
|
605
|
+
status: string;
|
|
606
|
+
written: boolean;
|
|
607
|
+
}>>;
|
|
608
|
+
export declare function fetchExaminerReport(studyId: string): Promise<ApiResponse<ExaminerReportData>>;
|
package/console/dist/ui/api.js
CHANGED
|
@@ -1027,3 +1027,59 @@ export { request,
|
|
|
1027
1027
|
getToken, setToken, clearToken, checkAuth, approveApproval, rejectApproval, editApproval, fetchPrinciples, fetchCorePrinciples, fetchPrincipleDetail, fetchPrincipleGovernance, fetchOwnerDecisionView, fetchOwnerDecisionInbox, fetchOwnerDecisionLibrary, fetchPrincipleTrajectory, fetchPrincipleReceipts, fetchReceiptCounts, fetchEvidenceAudit, archivePrinciple, unarchivePrinciple, createFeedbackReport, listFeedbackReports, getFeedbackReport, deleteFeedbackReport,
|
|
1028
1028
|
// Feedback submit ladder (Slice 3, spec §8)
|
|
1029
1029
|
fetchFeedbackChannels, submitFeedbackReport, markFeedbackReportSent, fetchConfigSummary, fetchConfigCatalog, updateAgentBinding, updateDefaultRuntime, createRuntimeProfile, updateRuntimeProfile, deleteRuntimeProfile, fetchAgentReadiness, patchFeatureFlag, fetchOutputLanguage, updateOutputLanguage, fetchWorkspaces, addWorkspace, removeWorkspace, syncWorkspace, fetchGovernanceQueue, fetchGovernanceExperience, recoverFailedTask, fetchFailedTaskDetail, fetchOwnerDecisions, resolveOwnerDecision, fetchApprovalsGrouped, fetchAllActivations, disableActivation, proposePromptRevision, fetchRuleCodeOwnerReview, ruleCodeDecision, pauseAllRuleCode, releaseRuleCodePause, fetchLifecycleMetrics, fetchUpdateStatus, fetchUpdateHistory, applyFullUpdate, fetchUpdateRecovery, fetchUpdateTransaction, fetchEvidenceChain, fetchIntentSummary, fetchIntentContent, createIntentTemplate, saveIntentContent, fetchIntentVersions, recordIntentDecision, listIntentDecisionsByPainId, listIntentDecisionsByTaskId, fetchIntentDecisionSummary, dispatchFollowUp, fetchDesignGateSessions, fetchDesignGateDeclaration, createDesignGateDeclaration, updateDesignGateDecision, revokeDesignGateDeclaration, };
|
|
1030
|
+
export async function fetchExaminerOverview() {
|
|
1031
|
+
return request('/api/v1/examiner/overview', undefined, (v) => {
|
|
1032
|
+
if (v !== null && typeof v === 'object' && Object.hasOwn(v, 'studies'))
|
|
1033
|
+
return v;
|
|
1034
|
+
return null;
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
export async function fetchExaminerPrinciples() {
|
|
1038
|
+
return request('/api/v1/examiner/principles', undefined, (v) => {
|
|
1039
|
+
if (v && typeof v === 'object' && Array.isArray(v.principles))
|
|
1040
|
+
return v;
|
|
1041
|
+
return null;
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
export async function createExaminerStudy(input) {
|
|
1045
|
+
return request('/api/v1/examiner/studies', { method: 'POST', body: JSON.stringify(input) }, (v) => {
|
|
1046
|
+
if (v && typeof v.studyId === 'string')
|
|
1047
|
+
return v;
|
|
1048
|
+
return null;
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
export async function fetchExaminerStudyStatus(studyId) {
|
|
1052
|
+
return request(`/api/v1/examiner/studies/${encodeURIComponent(studyId)}`, undefined, (v) => {
|
|
1053
|
+
if (v && typeof v.studyId === 'string')
|
|
1054
|
+
return v;
|
|
1055
|
+
return null;
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
export async function runExaminerPreflight(studyId) {
|
|
1059
|
+
return request(`/api/v1/examiner/studies/${encodeURIComponent(studyId)}/preflight`, { method: 'POST' }, (v) => {
|
|
1060
|
+
if (v && typeof v.ok === 'boolean')
|
|
1061
|
+
return v;
|
|
1062
|
+
return null;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
export async function runExaminerStudy(studyId) {
|
|
1066
|
+
return request(`/api/v1/examiner/studies/${encodeURIComponent(studyId)}/run`, { method: 'POST', body: JSON.stringify({ mode: 'offline-stub' }) }, (v) => {
|
|
1067
|
+
if (v && Array.isArray(v.units))
|
|
1068
|
+
return v;
|
|
1069
|
+
return null;
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
export async function generateExaminerReport(studyId) {
|
|
1073
|
+
return request(`/api/v1/examiner/studies/${encodeURIComponent(studyId)}/report`, { method: 'POST' }, (v) => {
|
|
1074
|
+
if (v && typeof v.status === 'string')
|
|
1075
|
+
return v;
|
|
1076
|
+
return null;
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
export async function fetchExaminerReport(studyId) {
|
|
1080
|
+
return request(`/api/v1/examiner/studies/${encodeURIComponent(studyId)}/report`, undefined, (v) => {
|
|
1081
|
+
if (v && v.report)
|
|
1082
|
+
return v;
|
|
1083
|
+
return null;
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* P2-1: the Examiner is a DEV-only research surface. The navigation entry is
|
|
4
|
+
* derived from the build-time mode, so a production bundle never advertises a
|
|
5
|
+
* capability the server refuses.
|
|
6
|
+
*/
|
|
7
|
+
export declare function visibleMainNavItems(isDev: boolean): {
|
|
8
|
+
id: string;
|
|
9
|
+
labelKey: string;
|
|
10
|
+
href: string;
|
|
11
|
+
icon: React.ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & React.RefAttributes<SVGSVGElement>>;
|
|
12
|
+
}[];
|
|
2
13
|
export declare function AppSidebar({ featureFlags }: {
|
|
3
14
|
featureFlags?: Record<string, {
|
|
4
15
|
enabled: boolean;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
|
4
|
-
import { Sun, Moon, Focus, AlertTriangle, AlertCircle, BookOpen, Zap, Archive, Compass, Settings, MessageSquare, RefreshCw, LogOut, Languages, Menu, } from "lucide-react";
|
|
4
|
+
import { Sun, Moon, Focus, AlertTriangle, AlertCircle, BookOpen, Zap, Archive, Compass, FlaskConical, Settings, MessageSquare, RefreshCw, LogOut, Languages, Menu, } from "lucide-react";
|
|
5
5
|
import { useTheme } from "../theme-provider.js";
|
|
6
6
|
import { useTranslation } from "react-i18next";
|
|
7
7
|
import { useNotifications } from "../notifications/useNotifications.js";
|
|
8
8
|
import { clearToken } from "../../api.js";
|
|
9
9
|
import { cn } from "../../../lib/utils.js";
|
|
10
|
+
import { IS_DEV } from "../../utils/runtime-mode.js";
|
|
10
11
|
import { Sheet, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "../ui/sheet.js";
|
|
11
12
|
function ThresholdMark({ className }) {
|
|
12
13
|
return (_jsxs("svg", { viewBox: "0 0 28 28", className: className, fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: [_jsx("path", { d: "M6 4V24M22 4V24M2 14H26", strokeLinecap: "square" }), _jsx("circle", { cx: "14", cy: "14", r: "2.5", fill: "currentColor", stroke: "none" })] }));
|
|
@@ -17,8 +18,17 @@ const mainNavItems = [
|
|
|
17
18
|
{ id: "principles", labelKey: "components.sidebar.principles", href: "/principles", icon: BookOpen },
|
|
18
19
|
{ id: "activation", labelKey: "components.sidebar.activation", href: "/activation", icon: Zap },
|
|
19
20
|
{ id: "debt", labelKey: "components.sidebar.debt", href: "/debt", icon: Archive },
|
|
21
|
+
{ id: "examiner", labelKey: "components.sidebar.examiner", href: "/examiner", icon: FlaskConical },
|
|
20
22
|
{ id: "intent", labelKey: "components.sidebar.intent", href: "/intent", icon: Compass },
|
|
21
23
|
];
|
|
24
|
+
/**
|
|
25
|
+
* P2-1: the Examiner is a DEV-only research surface. The navigation entry is
|
|
26
|
+
* derived from the build-time mode, so a production bundle never advertises a
|
|
27
|
+
* capability the server refuses.
|
|
28
|
+
*/
|
|
29
|
+
export function visibleMainNavItems(isDev) {
|
|
30
|
+
return isDev ? mainNavItems : mainNavItems.filter((item) => item.id !== "examiner");
|
|
31
|
+
}
|
|
22
32
|
const toolNavItems = [
|
|
23
33
|
{ id: "control-center", labelKey: "components.sidebar.controlCenter", href: "/control-center", icon: Settings },
|
|
24
34
|
{ id: "report-problem", labelKey: "components.sidebar.reportProblem", href: "/report-problem", icon: MessageSquare },
|
|
@@ -51,7 +61,7 @@ function SidebarContent({ featureFlags, onNavigate, touchTargets, }) {
|
|
|
51
61
|
i18n.changeLanguage(nextLang);
|
|
52
62
|
localStorage.setItem("pd-language", nextLang);
|
|
53
63
|
};
|
|
54
|
-
return (_jsxs(_Fragment, { children: [_jsxs("nav", { className: "flex-1 py-2 overflow-y-auto", "aria-label": t("components.sidebar.navigation", { defaultValue: "主导航" }), children: [_jsx("div", { className: "px-2", children:
|
|
64
|
+
return (_jsxs(_Fragment, { children: [_jsxs("nav", { className: "flex-1 py-2 overflow-y-auto", "aria-label": t("components.sidebar.navigation", { defaultValue: "主导航" }), children: [_jsx("div", { className: "px-2", children: visibleMainNavItems(IS_DEV).map((item) => {
|
|
55
65
|
const Icon = item.icon;
|
|
56
66
|
const active = isActive(item.href);
|
|
57
67
|
return (_jsxs(Link, { to: item.href, "data-testid": `nav-${item.id}`, "aria-current": active ? "page" : undefined, onClick: onNavigate, className: cn("flex items-center gap-3 px-3 rounded-[var(--radius-sm)] text-[13px] transition-all duration-150", touchTargets ? "min-h-[44px] py-2" : "py-2", active
|