@stravica-ai/digital-operator-pa 0.1.55 → 0.1.56
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/dist/engineering-pa-cli/src/pa/work-cli.mjs +116 -0
- package/dist/engineering-pa-cli/src/research/engine/finalise.mjs +14 -4
- package/dist/engineering-pa-cli/src/research/workflows/research-deep.js +15 -5
- package/dist/engineering-pa-cli/src/research/workflows/research-normal.js +60 -14
- package/dist/engineering-pa-cli/src/research/wrappers/normal.mjs +46 -10
- package/dist/engineering-pa-core/roles/agentic-workflow-designer/definition.md +1 -1
- package/dist/engineering-pa-workspace/app/assets/brand/icons/app-icon-192.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/app-icon-512.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/apple-touch-icon.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon-16.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon-32.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon-48.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon.ico +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/favicon.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/maskable-icon-512.png +0 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/maskable-icon.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/icons/safari-pinned-tab.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/stravica-logo-dark.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/stravica-logo-light.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/brand/stravica-mark.svg +1 -0
- package/dist/engineering-pa-workspace/app/assets/modal.js +17 -17
- package/dist/engineering-pa-workspace/app/assets/theme.css +332 -99
- package/dist/engineering-pa-workspace/app/assets/theme.js +259 -107
- package/dist/engineering-pa-workspace/app/banner.js +16 -12
- package/dist/engineering-pa-workspace/app/delegations.html +3 -3
- package/dist/engineering-pa-workspace/app/home.html +2 -2
- package/dist/engineering-pa-workspace/app/index.html +6 -3
- package/dist/engineering-pa-workspace/app/knowledge-map.html +2 -2
- package/dist/engineering-pa-workspace/app/knowledge.html +6 -6
- package/dist/engineering-pa-workspace/app/notes.html +2 -2
- package/dist/engineering-pa-workspace/app/onboarding.html +12 -11
- package/dist/engineering-pa-workspace/app/profile.html +4 -4
- package/dist/engineering-pa-workspace/app/project-detail.html +18 -18
- package/dist/engineering-pa-workspace/app/projects.html +4 -4
- package/dist/engineering-pa-workspace/app/repos.html +15 -14
- package/dist/engineering-pa-workspace/app/review-annotator.js +38 -38
- package/dist/engineering-pa-workspace/app/reviews.html +7 -7
- package/dist/engineering-pa-workspace/app/schedules.html +11 -11
- package/dist/engineering-pa-workspace/app/secrets.html +6 -6
- package/dist/engineering-pa-workspace/app/staging.html +15 -15
- package/dist/engineering-pa-workspace/app/tasks.html +4 -4
- package/dist/engineering-pa-workspace/app/viewer.html +9 -9
- package/dist/engineering-pa-workspace/index.mjs +2 -0
- package/dist/engineering-pa-workspace/src/server.mjs +6 -6
- package/dist/engineering-pa-workspace/src/workspace-page-contract.mjs +47 -19
- package/package.json +1 -1
|
@@ -19,10 +19,14 @@
|
|
|
19
19
|
// fall-back-to-default case. Full rationale + tests in
|
|
20
20
|
// `@stravica-ai/engineering-pa-core/src/workItemId/index.mjs`.
|
|
21
21
|
|
|
22
|
+
import { existsSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
|
|
22
25
|
import { readPreferences } from '../../../engineering-pa-core/src/preferences.mjs';
|
|
23
26
|
|
|
24
27
|
import { createWork, setWorkStatus, listWork } from './work.mjs';
|
|
25
28
|
import { getRepoRoot } from './repo-root.mjs';
|
|
29
|
+
import { listSubdirs } from './io.mjs';
|
|
26
30
|
|
|
27
31
|
const USAGE = `pa work — manage operator-repo work items
|
|
28
32
|
|
|
@@ -154,6 +158,21 @@ async function doCreate(args) {
|
|
|
154
158
|
process.stderr.write(` accepted flags: ${known}\n`);
|
|
155
159
|
return 1;
|
|
156
160
|
}
|
|
161
|
+
// Project existence gate (w-2026-09-30-dave-005 estate chore). Before this gate,
|
|
162
|
+
// `pa work create --project <nonexistent>` silently minted a work item pointing at
|
|
163
|
+
// a project that did not exist; the mistake surfaced later as an orphan in listings.
|
|
164
|
+
// Refuse loudly with the nearest matches so a typo is a fast fix, not a silent
|
|
165
|
+
// corruption. Only the CLI enforces this — programmatic callers of createWork()
|
|
166
|
+
// stay permissive (test fixtures use --project 'proj-a' without a real project on
|
|
167
|
+
// disk); wider grep found no CLI caller depending on the old behaviour, so no
|
|
168
|
+
// escape hatch is exposed.
|
|
169
|
+
if (typeof flags.project === 'string') {
|
|
170
|
+
const projectErr = await validateProjectExists(flags.project);
|
|
171
|
+
if (projectErr) {
|
|
172
|
+
process.stderr.write(projectErr);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
157
176
|
const priority = flags.priority != null && flags.priority !== true
|
|
158
177
|
? Number(flags.priority)
|
|
159
178
|
: null;
|
|
@@ -226,3 +245,100 @@ async function doList(args) {
|
|
|
226
245
|
for (const r of rows) process.stdout.write(cols.map((c) => pad(r[c], widths[c])).join(' ') + '\n');
|
|
227
246
|
return 0;
|
|
228
247
|
}
|
|
248
|
+
|
|
249
|
+
// ── project-existence validation (w-2026-09-30-dave-005 estate chore) ──────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Levenshtein distance between two short strings. Used to rank nearest project ids
|
|
253
|
+
* for a typo; not exported.
|
|
254
|
+
*/
|
|
255
|
+
function levenshtein(a, b) {
|
|
256
|
+
if (a === b) return 0;
|
|
257
|
+
if (!a.length) return b.length;
|
|
258
|
+
if (!b.length) return a.length;
|
|
259
|
+
const prev = new Array(b.length + 1);
|
|
260
|
+
const curr = new Array(b.length + 1);
|
|
261
|
+
for (let j = 0; j <= b.length; j += 1) prev[j] = j;
|
|
262
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
263
|
+
curr[0] = i;
|
|
264
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
265
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
266
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
267
|
+
}
|
|
268
|
+
for (let j = 0; j <= b.length; j += 1) prev[j] = curr[j];
|
|
269
|
+
}
|
|
270
|
+
return prev[b.length];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* List every projects/<id>/ whose project.json exists on disk. Names only, sorted
|
|
275
|
+
* lexicographically. Empty when the projects/ dir is absent (accepted by listSubdirs).
|
|
276
|
+
*/
|
|
277
|
+
async function listExistingProjectIds() {
|
|
278
|
+
const repoRoot = await getRepoRoot();
|
|
279
|
+
const projectsDir = join(repoRoot, 'projects');
|
|
280
|
+
const subdirs = await listSubdirs(projectsDir);
|
|
281
|
+
const ids = [];
|
|
282
|
+
for (const name of subdirs) {
|
|
283
|
+
if (existsSync(join(projectsDir, name, 'project.json'))) ids.push(name);
|
|
284
|
+
}
|
|
285
|
+
ids.sort();
|
|
286
|
+
return ids;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Nearest candidates for a missing project id: substring matches first (both
|
|
291
|
+
* directions), then the closest by Levenshtein distance (up to length/2 + 1). At
|
|
292
|
+
* most 5 suggestions; sorted deterministically.
|
|
293
|
+
*/
|
|
294
|
+
function nearestProjectMatches(target, candidates, max = 5) {
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
const out = [];
|
|
297
|
+
const push = (id) => { if (!seen.has(id)) { seen.add(id); out.push(id); } };
|
|
298
|
+
for (const id of candidates) {
|
|
299
|
+
if (id.toLowerCase().includes(target.toLowerCase()) || target.toLowerCase().includes(id.toLowerCase())) {
|
|
300
|
+
push(id);
|
|
301
|
+
}
|
|
302
|
+
if (out.length >= max) break;
|
|
303
|
+
}
|
|
304
|
+
if (out.length < max) {
|
|
305
|
+
const threshold = Math.max(1, Math.floor(target.length / 2) + 1);
|
|
306
|
+
const scored = candidates
|
|
307
|
+
.filter((id) => !seen.has(id))
|
|
308
|
+
.map((id) => ({ id, d: levenshtein(target.toLowerCase(), id.toLowerCase()) }))
|
|
309
|
+
.filter((r) => r.d <= threshold)
|
|
310
|
+
.sort((a, b) => a.d - b.d || a.id.localeCompare(b.id));
|
|
311
|
+
for (const { id } of scored) {
|
|
312
|
+
push(id);
|
|
313
|
+
if (out.length >= max) break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Returns null when the project id resolves to an existing project.json on disk, or
|
|
321
|
+
* a preformatted stderr message (with trailing newline) when it does not. Split so
|
|
322
|
+
* doCreate can print + return exit 1 without repeating the plumbing.
|
|
323
|
+
*/
|
|
324
|
+
async function validateProjectExists(projectId) {
|
|
325
|
+
const repoRoot = await getRepoRoot();
|
|
326
|
+
const path = join(repoRoot, 'projects', projectId, 'project.json');
|
|
327
|
+
if (existsSync(path)) return null;
|
|
328
|
+
let candidates = [];
|
|
329
|
+
try { candidates = await listExistingProjectIds(); } catch { candidates = []; }
|
|
330
|
+
const nearest = nearestProjectMatches(projectId, candidates);
|
|
331
|
+
const lines = [`[pa work create] unknown project: ${projectId}`];
|
|
332
|
+
lines.push(` expected projects/${projectId}/project.json on disk (not found)`);
|
|
333
|
+
if (nearest.length) {
|
|
334
|
+
lines.push(` did you mean: ${nearest.join(', ')}`);
|
|
335
|
+
} else if (candidates.length) {
|
|
336
|
+
lines.push(` existing projects: ${candidates.slice(0, 10).join(', ')}${candidates.length > 10 ? ', ...' : ''}`);
|
|
337
|
+
} else {
|
|
338
|
+
lines.push(' no projects on disk yet — create one with `pa project create` first');
|
|
339
|
+
}
|
|
340
|
+
return lines.map((l) => `${l}\n`).join('');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Exported for tests only — pure helpers, no filesystem side-effects.
|
|
344
|
+
export const __testables = { levenshtein, nearestProjectMatches };
|
|
@@ -134,9 +134,13 @@ export async function degradedSynthAgent(ctx, sq, reason) {
|
|
|
134
134
|
|
|
135
135
|
// Pure coverage computation. Given the planner's sub-question ids and the synth
|
|
136
136
|
// receipts, decide which SQs have a finding (any receipt with written=true), which ran
|
|
137
|
-
// as a degraded write (empty finding,
|
|
138
|
-
// uncovered (no receipt at all
|
|
139
|
-
//
|
|
137
|
+
// as a degraded write (empty finding, covered on disk but zero surviving claims), and
|
|
138
|
+
// which are wholly uncovered (no receipt at all). `complete` is true only when every
|
|
139
|
+
// planned SQ has a receipt AND every receipt carries at least one claim — a degraded
|
|
140
|
+
// zero-claim finding is a coverage gap the wrapper must surface (status='partial'),
|
|
141
|
+
// not silently mark done (w-2026-09-30-dave-005). The wrapper still distinguishes the
|
|
142
|
+
// two failure modes: uncovered aborts finalise; degraded lets the answer.md be
|
|
143
|
+
// written and returns status='partial' with the degraded[] list.
|
|
140
144
|
export function coverageReport(subQuestionIds, synthReceipts) {
|
|
141
145
|
const receiptsBySq = new Map();
|
|
142
146
|
for (const r of synthReceipts || []) {
|
|
@@ -152,7 +156,13 @@ export function coverageReport(subQuestionIds, synthReceipts) {
|
|
|
152
156
|
covered.push(id);
|
|
153
157
|
if ((r.claimCount || 0) === 0) degraded.push(id);
|
|
154
158
|
}
|
|
155
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
planned,
|
|
161
|
+
covered,
|
|
162
|
+
degraded,
|
|
163
|
+
uncovered,
|
|
164
|
+
complete: uncovered.length === 0 && degraded.length === 0,
|
|
165
|
+
};
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
// -- Finalise (persist gate records, write sources.json + the session answer, round-trip, mark done) --
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED by assemble.mjs — DO NOT EDIT. Edit wrappers/deep.mjs + engine/*.mjs and re-run `pnpm assemble`.
|
|
2
2
|
// sources: wrappers/deep.mjs, engine/io.mjs, engine/plan.mjs, engine/agents.mjs, engine/finalise.mjs, engine/rank.mjs, engine/verify.mjs, engine/deepen.mjs, engine/deep.mjs
|
|
3
|
-
// contentHash: sha256:
|
|
3
|
+
// contentHash: sha256:b7c50f3547c79b4c63bb54f14d1c0675a9af3a1c871d7866050cd22ff3d28fb5
|
|
4
4
|
|
|
5
5
|
export const meta = {
|
|
6
6
|
name: 'research-deep',
|
|
@@ -511,9 +511,13 @@ async function degradedSynthAgent(ctx, sq, reason) {
|
|
|
511
511
|
|
|
512
512
|
// Pure coverage computation. Given the planner's sub-question ids and the synth
|
|
513
513
|
// receipts, decide which SQs have a finding (any receipt with written=true), which ran
|
|
514
|
-
// as a degraded write (empty finding,
|
|
515
|
-
// uncovered (no receipt at all
|
|
516
|
-
//
|
|
514
|
+
// as a degraded write (empty finding, covered on disk but zero surviving claims), and
|
|
515
|
+
// which are wholly uncovered (no receipt at all). `complete` is true only when every
|
|
516
|
+
// planned SQ has a receipt AND every receipt carries at least one claim — a degraded
|
|
517
|
+
// zero-claim finding is a coverage gap the wrapper must surface (status='partial'),
|
|
518
|
+
// not silently mark done (w-2026-09-30-dave-005). The wrapper still distinguishes the
|
|
519
|
+
// two failure modes: uncovered aborts finalise; degraded lets the answer.md be
|
|
520
|
+
// written and returns status='partial' with the degraded[] list.
|
|
517
521
|
function coverageReport(subQuestionIds, synthReceipts) {
|
|
518
522
|
const receiptsBySq = new Map();
|
|
519
523
|
for (const r of synthReceipts || []) {
|
|
@@ -529,7 +533,13 @@ function coverageReport(subQuestionIds, synthReceipts) {
|
|
|
529
533
|
covered.push(id);
|
|
530
534
|
if ((r.claimCount || 0) === 0) degraded.push(id);
|
|
531
535
|
}
|
|
532
|
-
return {
|
|
536
|
+
return {
|
|
537
|
+
planned,
|
|
538
|
+
covered,
|
|
539
|
+
degraded,
|
|
540
|
+
uncovered,
|
|
541
|
+
complete: uncovered.length === 0 && degraded.length === 0,
|
|
542
|
+
};
|
|
533
543
|
}
|
|
534
544
|
|
|
535
545
|
// -- Finalise (persist gate records, write sources.json + the session answer, round-trip, mark done) --
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED by assemble.mjs — DO NOT EDIT. Edit wrappers/normal.mjs + engine/*.mjs and re-run `pnpm assemble`.
|
|
2
2
|
// sources: wrappers/normal.mjs, engine/io.mjs, engine/plan.mjs, engine/agents.mjs, engine/finalise.mjs, engine/rank.mjs, engine/verify.mjs, engine/deepen.mjs
|
|
3
|
-
// contentHash: sha256:
|
|
3
|
+
// contentHash: sha256:9ea786a5296dd54d14bbc662d1d47a87f437829fc5d394c3a83dd64f65ff1236
|
|
4
4
|
|
|
5
5
|
export const meta = {
|
|
6
6
|
name: 'research-normal',
|
|
@@ -510,9 +510,13 @@ async function degradedSynthAgent(ctx, sq, reason) {
|
|
|
510
510
|
|
|
511
511
|
// Pure coverage computation. Given the planner's sub-question ids and the synth
|
|
512
512
|
// receipts, decide which SQs have a finding (any receipt with written=true), which ran
|
|
513
|
-
// as a degraded write (empty finding,
|
|
514
|
-
// uncovered (no receipt at all
|
|
515
|
-
//
|
|
513
|
+
// as a degraded write (empty finding, covered on disk but zero surviving claims), and
|
|
514
|
+
// which are wholly uncovered (no receipt at all). `complete` is true only when every
|
|
515
|
+
// planned SQ has a receipt AND every receipt carries at least one claim — a degraded
|
|
516
|
+
// zero-claim finding is a coverage gap the wrapper must surface (status='partial'),
|
|
517
|
+
// not silently mark done (w-2026-09-30-dave-005). The wrapper still distinguishes the
|
|
518
|
+
// two failure modes: uncovered aborts finalise; degraded lets the answer.md be
|
|
519
|
+
// written and returns status='partial' with the degraded[] list.
|
|
516
520
|
function coverageReport(subQuestionIds, synthReceipts) {
|
|
517
521
|
const receiptsBySq = new Map();
|
|
518
522
|
for (const r of synthReceipts || []) {
|
|
@@ -528,7 +532,13 @@ function coverageReport(subQuestionIds, synthReceipts) {
|
|
|
528
532
|
covered.push(id);
|
|
529
533
|
if ((r.claimCount || 0) === 0) degraded.push(id);
|
|
530
534
|
}
|
|
531
|
-
return {
|
|
535
|
+
return {
|
|
536
|
+
planned,
|
|
537
|
+
covered,
|
|
538
|
+
degraded,
|
|
539
|
+
uncovered,
|
|
540
|
+
complete: uncovered.length === 0 && degraded.length === 0,
|
|
541
|
+
};
|
|
532
542
|
}
|
|
533
543
|
|
|
534
544
|
// -- Finalise (persist gate records, write sources.json + the session answer, round-trip, mark done) --
|
|
@@ -823,7 +833,14 @@ async function runDeepenRound(ctx, state, { lenses = VERIFY_LENSES, quorum = 2,
|
|
|
823
833
|
// the parent session's 200/200 WebSearch budget — the tools shipped in agents.mjs are
|
|
824
834
|
// the same tool surface the parent uses, so caps have to leave headroom).
|
|
825
835
|
const NORMAL_SEARCH_TOOL_CAP = 12; // matches agents.searchAgent default
|
|
826
|
-
const NORMAL_CLAIM_BUDGET = 25; // matches
|
|
836
|
+
const NORMAL_CLAIM_BUDGET = 25; // matches rankAndCapPerSq call below
|
|
837
|
+
// Per-sub-question retention floor for normal mode (w-2026-09-30-dave-005). Sessions
|
|
838
|
+
// 01M3S5PXQTCZR70MGDQRZWN6ZM (4 SQs, sq-3/sq-4 zero-claim, budgetDropped 56, 5.47M
|
|
839
|
+
// tokens) and 01M3S55WDKSYSC7Z7S8VRRJ9PC (6 SQs, sq-3/sq-4 zero-claim, budgetDropped
|
|
840
|
+
// 75, 10.97M tokens) both had later sub-questions silently starved by the earlier
|
|
841
|
+
// global rankAndCap cap while coverage.complete still reported true. Floor is the
|
|
842
|
+
// same "top-1 per SQ" guarantee deep already uses (DEEP_RETAIN_FLOOR_PER_SQ).
|
|
843
|
+
const NORMAL_RETAIN_FLOOR_PER_SQ = 1;
|
|
827
844
|
function normalWebSearchCeiling(subQuestionCount, centralClaimCount) {
|
|
828
845
|
const searchers = subQuestionCount * NORMAL_SEARCH_TOOL_CAP;
|
|
829
846
|
const verifiers = centralClaimCount * VERIFY_LENSES.length * VERIFY_TOOL_CALL_CAP_DEFAULT;
|
|
@@ -847,10 +864,19 @@ phase('Search');
|
|
|
847
864
|
const sqResults = await runSearchExtract(ctx, subQuestions);
|
|
848
865
|
|
|
849
866
|
// -- Rank + budget cap (deterministic code — §4.1) --
|
|
867
|
+
// Per-SQ floor applied BEFORE global trimming (w-2026-09-30-dave-005): reserve at
|
|
868
|
+
// least NORMAL_RETAIN_FLOOR_PER_SQ retained claim(s) per sub-question that had any
|
|
869
|
+
// claim extracted, then fill the remainder by global rank. A pure global cap starves
|
|
870
|
+
// later sub-questions silently (they end with zero retained claims, hence a degraded
|
|
871
|
+
// finding) while the wrapper still marks done.
|
|
850
872
|
const urlBySourceId = new Map();
|
|
851
873
|
const allClaims = flattenClaims(sqResults, urlBySourceId);
|
|
852
|
-
const { retained, extractedCount, budgetDropped } =
|
|
853
|
-
|
|
874
|
+
const { retained, extractedCount, budgetDropped } = rankAndCapPerSq(
|
|
875
|
+
allClaims,
|
|
876
|
+
NORMAL_CLAIM_BUDGET, // 25 (harness-skill cap)
|
|
877
|
+
{ floorPerSq: NORMAL_RETAIN_FLOOR_PER_SQ },
|
|
878
|
+
);
|
|
879
|
+
log(`extracted ${extractedCount} claims; retained ${retained.length} (budgetDropped ${budgetDropped}, floorPerSq ${NORMAL_RETAIN_FLOOR_PER_SQ})`);
|
|
854
880
|
|
|
855
881
|
// -- Verify (3-vote adversarial on central claims only — §4 normal row) --
|
|
856
882
|
phase('Verify');
|
|
@@ -877,10 +903,16 @@ phase('Synthesize');
|
|
|
877
903
|
const synthReceipts = await runSynth(ctx, subQuestions, retained, verdictByClaimId);
|
|
878
904
|
|
|
879
905
|
// -- Coverage gate --
|
|
880
|
-
//
|
|
881
|
-
//
|
|
906
|
+
// Two failure modes, handled separately (w-2026-09-30-dave-005):
|
|
907
|
+
// 1. UNCOVERED — a planned SQ has no findings receipt at all. Abort finalise; no
|
|
908
|
+
// answer.md written; status='partial'. This is the original gate.
|
|
909
|
+
// 2. DEGRADED — every SQ has a receipt but one or more carry zero surviving claims
|
|
910
|
+
// (the per-SQ floor kicks in for the extractor pool, but verify/dedupe may still
|
|
911
|
+
// leave an SQ empty). Let finalise run so answer.md is written, then override the
|
|
912
|
+
// returned status to 'partial' and surface the degraded[] list. Do NOT mark done
|
|
913
|
+
// — a partial run is not a done run.
|
|
882
914
|
const coverage = coverageReport(subQuestions.map((s) => s.id), synthReceipts);
|
|
883
|
-
if (
|
|
915
|
+
if (coverage.uncovered.length) {
|
|
884
916
|
log(`ABORT: coverage gap — ${coverage.uncovered.length} of ${coverage.planned} sub-question(s) have no findings receipt: [${coverage.uncovered.join(', ')}]. Not marking done.`);
|
|
885
917
|
return {
|
|
886
918
|
sessionId: ctx.sessionId,
|
|
@@ -891,7 +923,7 @@ if (!coverage.complete) {
|
|
|
891
923
|
};
|
|
892
924
|
}
|
|
893
925
|
if (coverage.degraded.length) {
|
|
894
|
-
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, ${coverage.degraded.length} degraded (zero-claim finding): [${coverage.degraded.join(', ')}]`);
|
|
926
|
+
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, ${coverage.degraded.length} degraded (zero-claim finding): [${coverage.degraded.join(', ')}] — run will return status='partial'`);
|
|
895
927
|
} else {
|
|
896
928
|
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, all with substantive findings`);
|
|
897
929
|
}
|
|
@@ -905,6 +937,12 @@ const unsourcedRatio = totalClaimCount ? totalUnsourced / totalClaimCount : 0;
|
|
|
905
937
|
const sourcesJson = buildSourcesJson(sqResults);
|
|
906
938
|
const gateRecords = buildGateRecords(sqResults);
|
|
907
939
|
|
|
940
|
+
// Subagent token cost — surfaced in runStats + the returned summary so an operator can
|
|
941
|
+
// see what a normal-mode session actually spent, without opening the harness manifest
|
|
942
|
+
// (w-2026-09-30-dave-005). Same shape deep uses.
|
|
943
|
+
const outputTokensSpent = (typeof budget !== 'undefined' && budget && budget.spent) ? budget.spent() : 0;
|
|
944
|
+
const budgetTotal = (typeof budget !== 'undefined' && budget && budget.total != null) ? budget.total : null;
|
|
945
|
+
|
|
908
946
|
const runStats = {
|
|
909
947
|
extracted: extractedCount, central: centralClaims.length,
|
|
910
948
|
confirmed: confirmedN, refuted: refutedN, unverifiedInfra: infraN,
|
|
@@ -913,6 +951,8 @@ const runStats = {
|
|
|
913
951
|
gatedDropped: gateRecords.length,
|
|
914
952
|
coverage,
|
|
915
953
|
webSearchCeiling: ceilingUpperBound,
|
|
954
|
+
outputTokensSpent,
|
|
955
|
+
budgetTotal,
|
|
916
956
|
};
|
|
917
957
|
|
|
918
958
|
if (unsourcedRatio > UNSOURCED_ABORT_RATIO) {
|
|
@@ -924,12 +964,18 @@ if (unsourcedRatio > UNSOURCED_ABORT_RATIO) {
|
|
|
924
964
|
phase('Finalise');
|
|
925
965
|
const finalise = await runFinalise(ctx, { gateRecords, sourcesJson, runStats });
|
|
926
966
|
|
|
927
|
-
|
|
928
|
-
|
|
967
|
+
// A degraded run reports 'partial' regardless of the finaliser's markedDone flag —
|
|
968
|
+
// the answer.md is written and the session may be marked done in the store, but the
|
|
969
|
+
// returned summary tells the caller a sub-question was not answered
|
|
970
|
+
// (w-2026-09-30-dave-005). coverage.complete is already false in that case.
|
|
971
|
+
const baseStatus = finalise.markedDone ? 'done' : 'in-progress';
|
|
972
|
+
const status = coverage.degraded.length ? 'partial' : baseStatus;
|
|
973
|
+
log(`finalise: sources=${finalise.wroteSources} answer=${finalise.wroteAnswer} roundTrip=${finalise.roundTripOk} markedDone=${finalise.markedDone} gatesAppended=${finalise.gatesAppended ?? 0}/${gateRecords.length} status=${status}`);
|
|
929
974
|
|
|
930
975
|
return {
|
|
931
976
|
sessionId: ctx.sessionId,
|
|
932
977
|
status,
|
|
978
|
+
...(coverage.degraded.length ? { reason: `${coverage.degraded.length} sub-question(s) with zero surviving claims`, degraded: coverage.degraded } : {}),
|
|
933
979
|
question: ctx.baseQuestion,
|
|
934
980
|
outputMode: ctx.baseOutputMode,
|
|
935
981
|
subQuestions: subQuestions.map((s) => ({ id: s.id, text: s.text, kind: s.kind })),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { deriveContext } from '../engine/io.mjs';
|
|
5
5
|
import { runPlan } from '../engine/plan.mjs';
|
|
6
6
|
import { runSearchExtract } from '../engine/agents.mjs';
|
|
7
|
-
import { flattenClaims,
|
|
7
|
+
import { flattenClaims, rankAndCapPerSq } from '../engine/rank.mjs';
|
|
8
8
|
import { runVerify, VERIFY_LENSES, VERIFY_TOOL_CALL_CAP_DEFAULT } from '../engine/verify.mjs';
|
|
9
9
|
import { runDeepenRound } from '../engine/deepen.mjs';
|
|
10
10
|
import { UNSOURCED_ABORT_RATIO, buildSourcesJson, buildGateRecords, runSynth, runFinalise, coverageReport } from '../engine/finalise.mjs';
|
|
@@ -16,7 +16,14 @@ import { UNSOURCED_ABORT_RATIO, buildSourcesJson, buildGateRecords, runSynth, ru
|
|
|
16
16
|
// the parent session's 200/200 WebSearch budget — the tools shipped in agents.mjs are
|
|
17
17
|
// the same tool surface the parent uses, so caps have to leave headroom).
|
|
18
18
|
const NORMAL_SEARCH_TOOL_CAP = 12; // matches agents.searchAgent default
|
|
19
|
-
const NORMAL_CLAIM_BUDGET = 25; // matches
|
|
19
|
+
const NORMAL_CLAIM_BUDGET = 25; // matches rankAndCapPerSq call below
|
|
20
|
+
// Per-sub-question retention floor for normal mode (w-2026-09-30-dave-005). Sessions
|
|
21
|
+
// 01M3S5PXQTCZR70MGDQRZWN6ZM (4 SQs, sq-3/sq-4 zero-claim, budgetDropped 56, 5.47M
|
|
22
|
+
// tokens) and 01M3S55WDKSYSC7Z7S8VRRJ9PC (6 SQs, sq-3/sq-4 zero-claim, budgetDropped
|
|
23
|
+
// 75, 10.97M tokens) both had later sub-questions silently starved by the earlier
|
|
24
|
+
// global rankAndCap cap while coverage.complete still reported true. Floor is the
|
|
25
|
+
// same "top-1 per SQ" guarantee deep already uses (DEEP_RETAIN_FLOOR_PER_SQ).
|
|
26
|
+
const NORMAL_RETAIN_FLOOR_PER_SQ = 1;
|
|
20
27
|
function normalWebSearchCeiling(subQuestionCount, centralClaimCount) {
|
|
21
28
|
const searchers = subQuestionCount * NORMAL_SEARCH_TOOL_CAP;
|
|
22
29
|
const verifiers = centralClaimCount * VERIFY_LENSES.length * VERIFY_TOOL_CALL_CAP_DEFAULT;
|
|
@@ -54,10 +61,19 @@ phase('Search');
|
|
|
54
61
|
const sqResults = await runSearchExtract(ctx, subQuestions);
|
|
55
62
|
|
|
56
63
|
// -- Rank + budget cap (deterministic code — §4.1) --
|
|
64
|
+
// Per-SQ floor applied BEFORE global trimming (w-2026-09-30-dave-005): reserve at
|
|
65
|
+
// least NORMAL_RETAIN_FLOOR_PER_SQ retained claim(s) per sub-question that had any
|
|
66
|
+
// claim extracted, then fill the remainder by global rank. A pure global cap starves
|
|
67
|
+
// later sub-questions silently (they end with zero retained claims, hence a degraded
|
|
68
|
+
// finding) while the wrapper still marks done.
|
|
57
69
|
const urlBySourceId = new Map();
|
|
58
70
|
const allClaims = flattenClaims(sqResults, urlBySourceId);
|
|
59
|
-
const { retained, extractedCount, budgetDropped } =
|
|
60
|
-
|
|
71
|
+
const { retained, extractedCount, budgetDropped } = rankAndCapPerSq(
|
|
72
|
+
allClaims,
|
|
73
|
+
NORMAL_CLAIM_BUDGET, // 25 (harness-skill cap)
|
|
74
|
+
{ floorPerSq: NORMAL_RETAIN_FLOOR_PER_SQ },
|
|
75
|
+
);
|
|
76
|
+
log(`extracted ${extractedCount} claims; retained ${retained.length} (budgetDropped ${budgetDropped}, floorPerSq ${NORMAL_RETAIN_FLOOR_PER_SQ})`);
|
|
61
77
|
|
|
62
78
|
// -- Verify (3-vote adversarial on central claims only — §4 normal row) --
|
|
63
79
|
phase('Verify');
|
|
@@ -84,10 +100,16 @@ phase('Synthesize');
|
|
|
84
100
|
const synthReceipts = await runSynth(ctx, subQuestions, retained, verdictByClaimId);
|
|
85
101
|
|
|
86
102
|
// -- Coverage gate --
|
|
87
|
-
//
|
|
88
|
-
//
|
|
103
|
+
// Two failure modes, handled separately (w-2026-09-30-dave-005):
|
|
104
|
+
// 1. UNCOVERED — a planned SQ has no findings receipt at all. Abort finalise; no
|
|
105
|
+
// answer.md written; status='partial'. This is the original gate.
|
|
106
|
+
// 2. DEGRADED — every SQ has a receipt but one or more carry zero surviving claims
|
|
107
|
+
// (the per-SQ floor kicks in for the extractor pool, but verify/dedupe may still
|
|
108
|
+
// leave an SQ empty). Let finalise run so answer.md is written, then override the
|
|
109
|
+
// returned status to 'partial' and surface the degraded[] list. Do NOT mark done
|
|
110
|
+
// — a partial run is not a done run.
|
|
89
111
|
const coverage = coverageReport(subQuestions.map((s) => s.id), synthReceipts);
|
|
90
|
-
if (
|
|
112
|
+
if (coverage.uncovered.length) {
|
|
91
113
|
log(`ABORT: coverage gap — ${coverage.uncovered.length} of ${coverage.planned} sub-question(s) have no findings receipt: [${coverage.uncovered.join(', ')}]. Not marking done.`);
|
|
92
114
|
return {
|
|
93
115
|
sessionId: ctx.sessionId,
|
|
@@ -98,7 +120,7 @@ if (!coverage.complete) {
|
|
|
98
120
|
};
|
|
99
121
|
}
|
|
100
122
|
if (coverage.degraded.length) {
|
|
101
|
-
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, ${coverage.degraded.length} degraded (zero-claim finding): [${coverage.degraded.join(', ')}]`);
|
|
123
|
+
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, ${coverage.degraded.length} degraded (zero-claim finding): [${coverage.degraded.join(', ')}] — run will return status='partial'`);
|
|
102
124
|
} else {
|
|
103
125
|
log(`coverage: ${coverage.covered.length}/${coverage.planned} covered, all with substantive findings`);
|
|
104
126
|
}
|
|
@@ -112,6 +134,12 @@ const unsourcedRatio = totalClaimCount ? totalUnsourced / totalClaimCount : 0;
|
|
|
112
134
|
const sourcesJson = buildSourcesJson(sqResults);
|
|
113
135
|
const gateRecords = buildGateRecords(sqResults);
|
|
114
136
|
|
|
137
|
+
// Subagent token cost — surfaced in runStats + the returned summary so an operator can
|
|
138
|
+
// see what a normal-mode session actually spent, without opening the harness manifest
|
|
139
|
+
// (w-2026-09-30-dave-005). Same shape deep uses.
|
|
140
|
+
const outputTokensSpent = (typeof budget !== 'undefined' && budget && budget.spent) ? budget.spent() : 0;
|
|
141
|
+
const budgetTotal = (typeof budget !== 'undefined' && budget && budget.total != null) ? budget.total : null;
|
|
142
|
+
|
|
115
143
|
const runStats = {
|
|
116
144
|
extracted: extractedCount, central: centralClaims.length,
|
|
117
145
|
confirmed: confirmedN, refuted: refutedN, unverifiedInfra: infraN,
|
|
@@ -120,6 +148,8 @@ const runStats = {
|
|
|
120
148
|
gatedDropped: gateRecords.length,
|
|
121
149
|
coverage,
|
|
122
150
|
webSearchCeiling: ceilingUpperBound,
|
|
151
|
+
outputTokensSpent,
|
|
152
|
+
budgetTotal,
|
|
123
153
|
};
|
|
124
154
|
|
|
125
155
|
if (unsourcedRatio > UNSOURCED_ABORT_RATIO) {
|
|
@@ -131,12 +161,18 @@ if (unsourcedRatio > UNSOURCED_ABORT_RATIO) {
|
|
|
131
161
|
phase('Finalise');
|
|
132
162
|
const finalise = await runFinalise(ctx, { gateRecords, sourcesJson, runStats });
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
// A degraded run reports 'partial' regardless of the finaliser's markedDone flag —
|
|
165
|
+
// the answer.md is written and the session may be marked done in the store, but the
|
|
166
|
+
// returned summary tells the caller a sub-question was not answered
|
|
167
|
+
// (w-2026-09-30-dave-005). coverage.complete is already false in that case.
|
|
168
|
+
const baseStatus = finalise.markedDone ? 'done' : 'in-progress';
|
|
169
|
+
const status = coverage.degraded.length ? 'partial' : baseStatus;
|
|
170
|
+
log(`finalise: sources=${finalise.wroteSources} answer=${finalise.wroteAnswer} roundTrip=${finalise.roundTripOk} markedDone=${finalise.markedDone} gatesAppended=${finalise.gatesAppended ?? 0}/${gateRecords.length} status=${status}`);
|
|
136
171
|
|
|
137
172
|
return {
|
|
138
173
|
sessionId: ctx.sessionId,
|
|
139
174
|
status,
|
|
175
|
+
...(coverage.degraded.length ? { reason: `${coverage.degraded.length} sub-question(s) with zero surviving claims`, degraded: coverage.degraded } : {}),
|
|
140
176
|
question: ctx.baseQuestion,
|
|
141
177
|
outputMode: ctx.baseOutputMode,
|
|
142
178
|
subQuestions: subQuestions.map((s) => ({ id: s.id, text: s.text, kind: s.kind })),
|
|
@@ -95,7 +95,7 @@ Standing rules for the lane invocation, whatever engine the estate binds:
|
|
|
95
95
|
|
|
96
96
|
- The lane is invoked as a **tool call from the designer's own session** (a CLI such as `codex exec`), never as an agent the designer spawns. Where an estate binds the lane to an agent role, the dispatching PA runs that role as a sibling dispatch and the designer verifies the render on a second pass.
|
|
97
97
|
- The engine and model are **pinned explicitly per invocation**; never rely on a CLI config default.
|
|
98
|
-
- **
|
|
98
|
+
- **Follow the instance's LLM credential policy** (subscription sign-in, API key, or other vendor key as the instance permits); if the tooling asks for a credential shape the instance does not permit, stop and escalate.
|
|
99
99
|
- **Bounded iteration**: one build invocation plus at most one refinement.
|
|
100
100
|
- **On lane failure, stop and report.** Never silently self-author the design on the orchestrating model; the parent re-routes.
|
|
101
101
|
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img"><title>Stravica favicon</title><desc></desc><rect x="0" y="0" width="64" height="64" rx="14" fill="#080F19"/><g fill="#7295FF"><path d="M38 9L17 21V32L38 20Z"/><path d="M30 27L46 36V46L30 37L23 33Z"/><path d="M46 46L25 58V47L37 40Z"/></g></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512" role="img"><title>Stravica maskable app icon</title><desc></desc><rect x="0" y="0" width="512" height="512" rx="0" fill="#080F19"/><defs><linearGradient id="ma" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#174CFF"/><stop offset="1" stop-color="#263BFF"/></linearGradient><linearGradient id="mb" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#1674FF"/><stop offset="1" stop-color="#2854FF"/></linearGradient></defs><g transform="translate(167 126) scale(1.3888888888888888)"><path fill="url(#ma)" d="M86 4 L10 48 Q4 51 4 58 L4 81 Q4 87 10 84 L80 45 Q86 42 86 35 Z"/><path fill="url(#ma)" d="M88 108 L129 125 L47 172 Q41 176 41 170 L41 142 Q41 136 47 133 Z"/><path fill="url(#mb)" d="M41 84 L74 65 Q78 63 83 66 L124 88 Q130 91 130 98 L130 121 Q130 126 124 123 Z"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 136 180" width="136" height="180" role="img"><title>Stravica pinned tab</title><desc></desc><g transform="translate(0 0) scale(1.0)"><path fill="#000000" d="M86 4 L10 48 Q4 51 4 58 L4 81 Q4 87 10 84 L80 45 Q86 42 86 35 Z"/><path fill="#000000" d="M88 108 L129 125 L47 172 Q41 176 41 170 L41 142 Q41 136 47 133 Z"/><path fill="#000000" d="M41 84 L74 65 Q78 63 83 66 L124 88 Q130 91 130 98 L130 121 Q130 126 124 123 Z"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 180" width="760" height="180" role="img"><title>Stravica logo for dark backgrounds</title><desc></desc><g transform="translate(0 0) scale(1.0)"><defs><linearGradient id="la" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#174CFF"/><stop offset="1" stop-color="#263BFF"/></linearGradient><linearGradient id="lb" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#1674FF"/><stop offset="1" stop-color="#2854FF"/></linearGradient></defs><g transform="translate(0 0) scale(1.0)"><path fill="url(#la)" d="M86 4 L10 48 Q4 51 4 58 L4 81 Q4 87 10 84 L80 45 Q86 42 86 35 Z"/><path fill="url(#la)" d="M88 108 L129 125 L47 172 Q41 176 41 170 L41 142 Q41 136 47 133 Z"/><path fill="url(#lb)" d="M41 84 L74 65 Q78 63 83 66 L124 88 Q130 91 130 98 L130 121 Q130 126 124 123 Z"/></g><g fill="#F2F5FA" fill-rule="evenodd" transform="translate(178 49) scale(1.036697247706422)"><path d="M56 0H22C8 0 1 9 1 20C1 31 9 38 22 38H34C40 38 42 40 42 44C42 47 39 49 34 49H6L0 62H34C49 62 57 54 57 43C57 32 50 25 36 25H23C17 25 15 23 15 19C15 15 18 13 23 13H51Z" transform="translate(0 0)"/><path d="M0 0H56V14H35V62H21V14H0Z" transform="translate(71 0)"/><path d="M0 0H32C48 0 56 8 56 20C56 29 51 35 44 38L60 62H42L28 40H15V62H0ZM15 13V27H31C38 27 41 25 41 20C41 15 38 13 31 13Z" transform="translate(140 0)"/><path d="M0 62L28 0H41L69 62H53L34.5 20L16 62Z" transform="translate(213 0)"/><path d="M0 0H16L35 42L54 0H70L42 62H28Z" transform="translate(295 0)"/><path d="M0 0H15V62H0Z" transform="translate(378 0)"/><path d="M57 0H31C12 0 0 13 0 31C0 49 12 62 31 62H55V48H31C21 48 15 41 15 31C15 21 21 14 31 14H51Z" transform="translate(406 0)"/><path d="M0 62L28 0H41L69 62H53L34.5 20L16 62Z" transform="translate(476 0)"/></g></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 180" width="760" height="180" role="img"><title>Stravica logo for light backgrounds</title><desc></desc><g transform="translate(0 0) scale(1.0)"><defs><linearGradient id="la" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#174CFF"/><stop offset="1" stop-color="#263BFF"/></linearGradient><linearGradient id="lb" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#1674FF"/><stop offset="1" stop-color="#2854FF"/></linearGradient></defs><g transform="translate(0 0) scale(1.0)"><path fill="url(#la)" d="M86 4 L10 48 Q4 51 4 58 L4 81 Q4 87 10 84 L80 45 Q86 42 86 35 Z"/><path fill="url(#la)" d="M88 108 L129 125 L47 172 Q41 176 41 170 L41 142 Q41 136 47 133 Z"/><path fill="url(#lb)" d="M41 84 L74 65 Q78 63 83 66 L124 88 Q130 91 130 98 L130 121 Q130 126 124 123 Z"/></g><g fill="#080F19" fill-rule="evenodd" transform="translate(178 49) scale(1.036697247706422)"><path d="M56 0H22C8 0 1 9 1 20C1 31 9 38 22 38H34C40 38 42 40 42 44C42 47 39 49 34 49H6L0 62H34C49 62 57 54 57 43C57 32 50 25 36 25H23C17 25 15 23 15 19C15 15 18 13 23 13H51Z" transform="translate(0 0)"/><path d="M0 0H56V14H35V62H21V14H0Z" transform="translate(71 0)"/><path d="M0 0H32C48 0 56 8 56 20C56 29 51 35 44 38L60 62H42L28 40H15V62H0ZM15 13V27H31C38 27 41 25 41 20C41 15 38 13 31 13Z" transform="translate(140 0)"/><path d="M0 62L28 0H41L69 62H53L34.5 20L16 62Z" transform="translate(213 0)"/><path d="M0 0H16L35 42L54 0H70L42 62H28Z" transform="translate(295 0)"/><path d="M0 0H15V62H0Z" transform="translate(378 0)"/><path d="M57 0H31C12 0 0 13 0 31C0 49 12 62 31 62H55V48H31C21 48 15 41 15 31C15 21 21 14 31 14H51Z" transform="translate(406 0)"/><path d="M0 62L28 0H41L69 62H53L34.5 20L16 62Z" transform="translate(476 0)"/></g></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 136 180" width="136" height="180" role="img"><title>Stravica ribbon symbol</title><desc></desc><defs><linearGradient id="ma" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#174CFF"/><stop offset="1" stop-color="#263BFF"/></linearGradient><linearGradient id="mb" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#1674FF"/><stop offset="1" stop-color="#2854FF"/></linearGradient></defs><g transform="translate(0 0) scale(1.0)"><path fill="url(#ma)" d="M86 4 L10 48 Q4 51 4 58 L4 81 Q4 87 10 84 L80 45 Q86 42 86 35 Z"/><path fill="url(#ma)" d="M88 108 L129 125 L47 172 Q41 176 41 170 L41 142 Q41 136 47 133 Z"/><path fill="url(#mb)" d="M41 84 L74 65 Q78 63 83 66 L124 88 Q130 91 130 98 L130 121 Q130 126 124 123 Z"/></g></svg>
|
|
@@ -20,30 +20,30 @@
|
|
|
20
20
|
'use strict';
|
|
21
21
|
|
|
22
22
|
var CSS =
|
|
23
|
-
'.ws-modal-overlay{display:none;position:fixed;inset:0;background:
|
|
23
|
+
'.ws-modal-overlay{display:none;position:fixed;inset:0;background:color-mix(in srgb, var(--text) 65%, transparent);z-index:200;align-items:center;justify-content:center;padding:1rem}' +
|
|
24
24
|
'.ws-modal-overlay.open{display:flex}' +
|
|
25
|
-
'.ws-modal{background:var(--bg-surface
|
|
26
|
-
'.ws-modal-header{padding:1rem 1.25rem 0.5rem;border-bottom:1px solid var(--border
|
|
27
|
-
'.ws-modal-title{color:var(--text-bright
|
|
28
|
-
'.ws-modal-close{background:var(--bg-highlight
|
|
29
|
-
'.ws-modal-close:hover{color:var(--text-bright
|
|
30
|
-
'.ws-modal-body{padding:1rem 1.25rem 1.25rem;overflow-y:auto;color:var(--text
|
|
31
|
-
'.ws-modal-body pre{background:var(--bg-highlight
|
|
32
|
-
'.ws-modal-body code{background:var(--bg-highlight
|
|
25
|
+
'.ws-modal{background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;max-width:780px;width:100%;max-height:85vh;display:flex;flex-direction:column;position:relative;font-family:inherit}' +
|
|
26
|
+
'.ws-modal-header{padding:1rem 1.25rem 0.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:0.75rem}' +
|
|
27
|
+
'.ws-modal-title{color:var(--text-bright);font-size:1rem;font-weight:600;flex:1;min-width:0;word-break:break-word}' +
|
|
28
|
+
'.ws-modal-close{background:var(--bg-highlight);border:1px solid var(--border);border-radius:4px;color:var(--text-dim);font-size:0.72rem;padding:0.25rem 0.6rem;cursor:pointer;font-family:inherit;flex-shrink:0}' +
|
|
29
|
+
'.ws-modal-close:hover{color:var(--text-bright);border-color:var(--accent)}' +
|
|
30
|
+
'.ws-modal-body{padding:1rem 1.25rem 1.25rem;overflow-y:auto;color:var(--text);font-size:0.8rem;line-height:1.6}' +
|
|
31
|
+
'.ws-modal-body pre{background:var(--bg-highlight);border:1px solid var(--border);border-radius:5px;padding:0.6rem;overflow-x:auto;font-size:0.72rem;white-space:pre-wrap;word-break:break-word}' +
|
|
32
|
+
'.ws-modal-body code{background:var(--bg-highlight);border:1px solid var(--border);border-radius:3px;padding:0.05rem 0.3rem;font-size:0.78em;color:var(--cyan)}' +
|
|
33
33
|
'.ws-modal-body pre code{background:transparent;border:none;padding:0;color:inherit}' +
|
|
34
|
-
'.ws-modal-body h1,.ws-modal-body h2,.ws-modal-body h3,.ws-modal-body h4{color:var(--text-bright
|
|
35
|
-
'.ws-modal-body h1{font-size:1rem}.ws-modal-body h2{font-size:0.9rem;border-bottom:1px solid var(--border
|
|
36
|
-
'.ws-modal-body h3{font-size:0.82rem;color:var(--purple
|
|
34
|
+
'.ws-modal-body h1,.ws-modal-body h2,.ws-modal-body h3,.ws-modal-body h4{color:var(--text-bright);margin:0.75rem 0 0.35rem}' +
|
|
35
|
+
'.ws-modal-body h1{font-size:1rem}.ws-modal-body h2{font-size:0.9rem;border-bottom:1px solid var(--border);padding-bottom:0.15rem}' +
|
|
36
|
+
'.ws-modal-body h3{font-size:0.82rem;color:var(--purple)}' +
|
|
37
37
|
'.ws-modal-body p{margin-bottom:0.5rem}' +
|
|
38
38
|
'.ws-modal-body ul,.ws-modal-body ol{padding-left:1.3rem;margin-bottom:0.5rem}' +
|
|
39
39
|
'.ws-modal-body li{margin-bottom:0.1rem}' +
|
|
40
|
-
'.ws-modal-body a{color:var(--accent
|
|
40
|
+
'.ws-modal-body a{color:var(--accent);text-decoration:none}' +
|
|
41
41
|
'.ws-modal-body a:hover{text-decoration:underline}' +
|
|
42
42
|
'.ws-modal-body .kv{display:grid;grid-template-columns:7rem 1fr;gap:0.35rem 0.75rem;font-size:0.76rem;margin-bottom:0.75rem}' +
|
|
43
|
-
'.ws-modal-body .kv .k{color:var(--text-dim
|
|
44
|
-
'.ws-modal-body .kv .v{color:var(--text
|
|
45
|
-
'.ws-modal-body .kv .v.mono{font-family:
|
|
46
|
-
'.ws-modal-body .section-label{color:var(--text-dim
|
|
43
|
+
'.ws-modal-body .kv .k{color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;font-size:0.64rem;padding-top:0.1rem}' +
|
|
44
|
+
'.ws-modal-body .kv .v{color:var(--text);word-break:break-word}' +
|
|
45
|
+
'.ws-modal-body .kv .v.mono{font-family:var(--font-mono);font-size:0.72rem;color:var(--cyan)}' +
|
|
46
|
+
'.ws-modal-body .section-label{color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;font-size:0.64rem;margin-top:0.75rem;margin-bottom:0.3rem}';
|
|
47
47
|
|
|
48
48
|
var initialised = false;
|
|
49
49
|
var keyHandlerAttached = false;
|