@bongos/core 1.19.316 → 1.19.318
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/.bongos-core.json +108 -33
- package/modules/economy/cost.js +84 -19
- package/modules/lifecycle/db-analytics.js +719 -0
- package/modules/lifecycle/db-claim-reads.js +165 -0
- package/modules/lifecycle/db-claims.js +934 -0
- package/modules/lifecycle/db-deps-criteria.js +441 -0
- package/modules/lifecycle/db-goals.js +763 -0
- package/modules/lifecycle/db-grade.js +233 -0
- package/modules/lifecycle/db-overrides.js +181 -0
- package/modules/lifecycle/db-rank-authz.js +446 -0
- package/modules/lifecycle/db-shared.js +115 -0
- package/modules/lifecycle/db-ship.js +994 -0
- package/modules/lifecycle/db-tasks.js +810 -0
- package/modules/lifecycle/db-versions.js +118 -0
- package/modules/lifecycle/db.js +167 -5564
- package/modules/lifecycle/publish-reconciler.js +7 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/coreB-carve-proof.sh +18 -9
- package/scripts/gds/fitness.js +34 -26
- package/scripts/gds/identity-strip.js +75 -0
- package/scripts/gds/provision.js +7 -3
- package/scripts/gds/run-unit-tests.js +8 -1
- package/scripts/gds/session-cost.js +16 -0
- package/scripts/gds/session-digest-build.js +261 -0
- package/scripts/gds/ship.js +7 -198
- package/src/module-api.js +5 -1
- package/tests/auto_satisfy_criteria.mjs +3 -2
- package/tests/dependencies_migration.mjs +3 -2
- package/tests/fitness.mjs +43 -0
- package/tests/goal_abuse_matrix.mjs +3 -2
- package/tests/goal_advisory.mjs +3 -2
- package/tests/government_claim_eligibility.mjs +3 -1
- package/tests/government_rank_sync.mjs +9 -2
- package/tests/helpers.mjs +19 -0
- package/tests/migration_allocation.mjs +3 -1
- package/tests/publish_reconciler.mjs +41 -1
- package/tests/rank_gate.mjs +3 -1
- package/tests/reward_gate.mjs +3 -1
- package/tests/session_cost.mjs +64 -0
- package/tests/session_cost_row_raise.mjs +139 -0
- package/tests/session_records.mjs +72 -0
package/scripts/gds/ship.js
CHANGED
|
@@ -386,205 +386,14 @@ const {
|
|
|
386
386
|
redactHighEntropy, collectEnvSecrets,
|
|
387
387
|
} = require('../../modules/security/secret-scrub');
|
|
388
388
|
|
|
389
|
-
// snippetOf
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const flat = text.replace(/\0/g, '').replace(/\s+/g, ' ').trim();
|
|
397
|
-
if (!flat) return undefined;
|
|
398
|
-
const cut = flat.length > max ? flat.slice(0, max) : flat;
|
|
399
|
-
const scrubbed = scrubSecrets(cut, extraSecrets);
|
|
400
|
-
return scrubbed || undefined;
|
|
401
|
-
}
|
|
389
|
+
// snippetOf + buildSessionDigest moved to ./session-digest-build (task 1003217,
|
|
390
|
+
// serving the de-monolith goal of task 1003203) — pure, separately tested, and
|
|
391
|
+
// shared with session-digest.js. Re-exported below so this file's public surface
|
|
392
|
+
// and tests/session_records.mjs are unchanged.
|
|
393
|
+
const {
|
|
394
|
+
snippetOf, buildSessionDigest, HARNESS_RETRY_RE, UNATTRIBUTED_MODEL,
|
|
395
|
+
} = require('./session-digest-build');
|
|
402
396
|
|
|
403
|
-
// buildSessionDigest — turn parsed Claude Code transcript entries into
|
|
404
|
-
// { startedAt, endedAt, durationSeconds, totalTokens, metrics, detail }.
|
|
405
|
-
// Pure (no I/O) + exported for unit tests. `detail` is per-turn metadata + short
|
|
406
|
-
// scrubbed snippets, capped at maxTurns (head+tail kept when over).
|
|
407
|
-
// Harness guardrail retries (task 959) — an is_error tool_result that is NOT real
|
|
408
|
-
// friction but the agent harness rejecting a tool call: an Edit/Write before the
|
|
409
|
-
// file was Read ("File has not been read yet"), or a foreground sleep ("Blocked:
|
|
410
|
-
// sleep"). These were counted in tool_error_count, inflating it and skewing the
|
|
411
|
-
// BFG's order=tool_errors ranking (top sessions ranked on benign retries). They
|
|
412
|
-
// now go into a separate harness_retry_count so tool_error_count stays NET — and
|
|
413
|
-
// because the sessions/search ordering reads metrics->>'tool_error_count'
|
|
414
|
-
// (src/bongos/db.js), that ordering automatically uses the net figure with no route
|
|
415
|
-
// change. (ADR 0027 lineage — the 6D session evaluator.)
|
|
416
|
-
const HARNESS_RETRY_RE =
|
|
417
|
-
/File has not been read yet|Read it first before (?:writing|editing)|Blocked:\s*sleep|sleep is blocked in (?:the )?foreground/i;
|
|
418
|
-
|
|
419
|
-
// task 1676 — the family bucket unlabeled token-bearing turns are folded into so
|
|
420
|
-
// the cost-plus reward (ADR 0054) prices the real spend instead of rounding to $0.
|
|
421
|
-
// A concrete, priceable model id (llm-pricing.js maps it to the opus family by
|
|
422
|
-
// substring) — NOT a fabricated dollar figure; it carries the REAL token counts
|
|
423
|
-
// ship.js already parsed, priced server-side like any other bucket.
|
|
424
|
-
const UNATTRIBUTED_MODEL = 'claude-opus-4-8';
|
|
425
|
-
|
|
426
|
-
function buildSessionDigest(entries, opts = {}) {
|
|
427
|
-
const extra = opts.extraSecrets || [];
|
|
428
|
-
const MAX_TURNS = opts.maxTurns || 4000;
|
|
429
|
-
const m = {
|
|
430
|
-
assistant_turns: 0,
|
|
431
|
-
user_turns: 0,
|
|
432
|
-
tool_use_count: 0,
|
|
433
|
-
tool_result_count: 0,
|
|
434
|
-
tool_error_count: 0, // NET real errors (harness guardrail retries excluded)
|
|
435
|
-
harness_retry_count: 0, // benign harness guardrail retries (task 959)
|
|
436
|
-
tokens: { input: 0, output: 0, cache_read: 0, cache_creation: 0 },
|
|
437
|
-
model_usage: {}, // per-model token buckets for the cost-plus reward (task 1005)
|
|
438
|
-
// task 1676: tokens from turns with NO model id (not '<synthetic>'). Folded
|
|
439
|
-
// into a priceable bucket after the loop, then stripped from the digest.
|
|
440
|
-
_unlabeled: { input: 0, output: 0, cache_read: 0, cache_creation: 0 },
|
|
441
|
-
};
|
|
442
|
-
const detail = [];
|
|
443
|
-
let firstTs = null;
|
|
444
|
-
let lastTs = null;
|
|
445
|
-
let i = 0;
|
|
446
|
-
for (const e of Array.isArray(entries) ? entries : []) {
|
|
447
|
-
if (!e || typeof e !== 'object') continue;
|
|
448
|
-
const ts = typeof e.timestamp === 'string' ? e.timestamp : null;
|
|
449
|
-
if (ts) {
|
|
450
|
-
if (!firstTs) firstTs = ts;
|
|
451
|
-
lastTs = ts;
|
|
452
|
-
}
|
|
453
|
-
const msg = e.message;
|
|
454
|
-
if (e.type === 'assistant' && msg) {
|
|
455
|
-
m.assistant_turns++;
|
|
456
|
-
const u = msg.usage || {};
|
|
457
|
-
const tin = u.input_tokens || 0;
|
|
458
|
-
const tout = u.output_tokens || 0;
|
|
459
|
-
const tcr = u.cache_read_input_tokens || 0;
|
|
460
|
-
const tcc = u.cache_creation_input_tokens || 0;
|
|
461
|
-
m.tokens.input += tin;
|
|
462
|
-
m.tokens.output += tout;
|
|
463
|
-
m.tokens.cache_read += tcr;
|
|
464
|
-
m.tokens.cache_creation += tcc;
|
|
465
|
-
// task 1005: per-model bucket for the cost-plus token reward, reusing the
|
|
466
|
-
// tin/tout/tcr/tcc already parsed above (no second pass). Skip synthetic
|
|
467
|
-
// turns (a grader 429 etc. emits no real model id AND no real spend).
|
|
468
|
-
const model = msg.model;
|
|
469
|
-
if (model === '<synthetic>') {
|
|
470
|
-
// synthetic: no real spend — exclude from BOTH model_usage and the
|
|
471
|
-
// task-1676 unlabeled-remainder fold below (don't price phantom tokens).
|
|
472
|
-
} else if (model) {
|
|
473
|
-
const mu = m.model_usage[model] || (m.model_usage[model] =
|
|
474
|
-
{ input: 0, output: 0, cache_read: 0, cache_creation: 0 });
|
|
475
|
-
mu.input += tin; mu.output += tout; mu.cache_read += tcr; mu.cache_creation += tcc;
|
|
476
|
-
} else {
|
|
477
|
-
// task 1676: a token-bearing turn with NO model id. Previously dropped
|
|
478
|
-
// from model_usage (→ total_tokens>0 but priced $0 on the dev-box/CI
|
|
479
|
-
// path; ADR 0097). Accumulate the unlabeled tokens so the digest can
|
|
480
|
-
// attribute them to a real, priceable family bucket after the loop.
|
|
481
|
-
m._unlabeled.input += tin; m._unlabeled.output += tout;
|
|
482
|
-
m._unlabeled.cache_read += tcr; m._unlabeled.cache_creation += tcc;
|
|
483
|
-
}
|
|
484
|
-
const tools = [];
|
|
485
|
-
let textLen = 0;
|
|
486
|
-
let firstText = '';
|
|
487
|
-
if (Array.isArray(msg.content)) {
|
|
488
|
-
for (const c of msg.content) {
|
|
489
|
-
if (c && c.type === 'tool_use') {
|
|
490
|
-
m.tool_use_count++;
|
|
491
|
-
tools.push(String(c.name || '?'));
|
|
492
|
-
} else if (c && c.type === 'text' && typeof c.text === 'string') {
|
|
493
|
-
textLen += c.text.length;
|
|
494
|
-
if (!firstText) firstText = c.text;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
const row = { i, role: 'assistant', ts, in: tin, out: tout, cache_read: tcr, cache_creation: tcc };
|
|
499
|
-
if (tools.length) row.tools = tools;
|
|
500
|
-
if (textLen) row.text_len = textLen;
|
|
501
|
-
const snip = snippetOf(firstText, extra);
|
|
502
|
-
if (snip) row.snippet = snip;
|
|
503
|
-
detail.push(row);
|
|
504
|
-
i++;
|
|
505
|
-
} else if (e.type === 'user' && msg) {
|
|
506
|
-
m.user_turns++;
|
|
507
|
-
let results = 0;
|
|
508
|
-
let errors = 0;
|
|
509
|
-
let harnessRetries = 0;
|
|
510
|
-
let firstResult = '';
|
|
511
|
-
if (Array.isArray(msg.content)) {
|
|
512
|
-
for (const c of msg.content) {
|
|
513
|
-
if (c && c.type === 'tool_result') {
|
|
514
|
-
results++;
|
|
515
|
-
m.tool_result_count++;
|
|
516
|
-
const text = typeof c.content === 'string'
|
|
517
|
-
? c.content
|
|
518
|
-
: Array.isArray(c.content)
|
|
519
|
-
? c.content.map((p) => (p && typeof p.text === 'string' ? p.text : '')).join(' ')
|
|
520
|
-
: '';
|
|
521
|
-
if (c.is_error) {
|
|
522
|
-
// Split benign harness guardrail retries out of the net error count
|
|
523
|
-
// so order=tool_errors ranks on real friction (task 959).
|
|
524
|
-
if (HARNESS_RETRY_RE.test(text)) {
|
|
525
|
-
harnessRetries++;
|
|
526
|
-
m.harness_retry_count++;
|
|
527
|
-
} else {
|
|
528
|
-
errors++;
|
|
529
|
-
m.tool_error_count++;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
if (!firstResult) firstResult = text;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
const row = { i, role: 'user', ts };
|
|
537
|
-
if (results) row.tool_results = results;
|
|
538
|
-
if (errors) row.tool_errors = errors;
|
|
539
|
-
if (harnessRetries) row.harness_retries = harnessRetries;
|
|
540
|
-
const snip = snippetOf(firstResult, extra);
|
|
541
|
-
if (snip) row.snippet = snip;
|
|
542
|
-
detail.push(row);
|
|
543
|
-
i++;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
m.total_tokens = m.tokens.input + m.tokens.output + m.tokens.cache_read + m.tokens.cache_creation;
|
|
547
|
-
// task 1676 — cost-plus reward zero-out fix. The cost-plus session reward
|
|
548
|
-
// (ADR 0054) prices `model_usage` server-side. model_usage is keyed on msg.model
|
|
549
|
-
// and SKIPS any token-bearing turn whose model id is missing. On the dev-box/CI
|
|
550
|
-
// agent path that gap was the difference between total_tokens>0 (so the digest
|
|
551
|
-
// passes the route's empty-session guard + gets stored) and model_usage={} (so
|
|
552
|
-
// priceModelUsage returns $0 → 0 drachmae) — ADR 0097's "upload no priced model
|
|
553
|
-
// usage" symptom. Fold the unlabeled remainder (NOT synthetic turns) into a real,
|
|
554
|
-
// priceable family bucket so the cost-plus reward reflects the true spend rather
|
|
555
|
-
// than silently dropping to $0. opus is the conservative family for an unlabeled
|
|
556
|
-
// turn (matches llm-pricing.js's unknown-model fallback) — this instance runs
|
|
557
|
-
// opus by default, and pricing an unlabeled turn as opus never UNDER-pays.
|
|
558
|
-
const un = m._unlabeled;
|
|
559
|
-
if (un.input + un.output + un.cache_read + un.cache_creation > 0) {
|
|
560
|
-
const fb = m.model_usage[UNATTRIBUTED_MODEL] || (m.model_usage[UNATTRIBUTED_MODEL] =
|
|
561
|
-
{ input: 0, output: 0, cache_read: 0, cache_creation: 0 });
|
|
562
|
-
fb.input += un.input; fb.output += un.output;
|
|
563
|
-
fb.cache_read += un.cache_read; fb.cache_creation += un.cache_creation;
|
|
564
|
-
}
|
|
565
|
-
delete m._unlabeled; // scratch — never persisted to the digest
|
|
566
|
-
let boundedDetail = detail;
|
|
567
|
-
if (detail.length > MAX_TURNS) {
|
|
568
|
-
const head = Math.floor(MAX_TURNS * 0.7);
|
|
569
|
-
const tail = MAX_TURNS - head;
|
|
570
|
-
boundedDetail = detail.slice(0, head).concat(detail.slice(detail.length - tail));
|
|
571
|
-
m.detail_truncated = true;
|
|
572
|
-
m.detail_total_turns = detail.length;
|
|
573
|
-
}
|
|
574
|
-
let durationSeconds = null;
|
|
575
|
-
if (firstTs && lastTs) {
|
|
576
|
-
const d = (Date.parse(lastTs) - Date.parse(firstTs)) / 1000;
|
|
577
|
-
if (Number.isFinite(d) && d >= 0) durationSeconds = Math.round(d);
|
|
578
|
-
}
|
|
579
|
-
return {
|
|
580
|
-
startedAt: firstTs,
|
|
581
|
-
endedAt: lastTs,
|
|
582
|
-
durationSeconds,
|
|
583
|
-
totalTokens: m.total_tokens,
|
|
584
|
-
metrics: m,
|
|
585
|
-
detail: boundedDetail,
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
397
|
|
|
589
398
|
// classifyUploadSkip — PURE decision for whether the session upload must skip,
|
|
590
399
|
// and the clear, actionable reason why. Separated from I/O so it is unit-tested
|
package/src/module-api.js
CHANGED
|
@@ -1059,8 +1059,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1059
1059
|
// landed since 1.19.314 with no explicit bump. run 32668969037. (task 1002620)
|
|
1060
1060
|
// 1.19.316 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1061
1061
|
// landed since 1.19.315 with no explicit bump. run 32669347711. (task 1002620)
|
|
1062
|
+
// 1.19.317 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1063
|
+
// landed since 1.19.316 with no explicit bump. run 32669571610. (task 1002620)
|
|
1064
|
+
// 1.19.318 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1065
|
+
// landed since 1.19.317 with no explicit bump. run 32669798745. (task 1002620)
|
|
1062
1066
|
// ---------------------------------------------------------------------------
|
|
1063
|
-
const CORE_VERSION = '1.19.
|
|
1067
|
+
const CORE_VERSION = '1.19.318'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1064
1068
|
|
|
1065
1069
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1066
1070
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import { strict as assert } from 'node:assert';
|
|
35
35
|
import { createRequire } from 'node:module';
|
|
36
36
|
import { readFileSync } from 'node:fs';
|
|
37
|
-
import { makeRunner } from './helpers.mjs';
|
|
37
|
+
import { makeRunner, lifecycleDbSource } from './helpers.mjs';
|
|
38
38
|
|
|
39
39
|
process.env.NODE_ENV = 'test';
|
|
40
40
|
const require = createRequire(import.meta.url);
|
|
@@ -113,7 +113,8 @@ await test('EXEC: every write goes through the caller-supplied exec (so a ship t
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
await test('EXEC: shipTask passes its transaction CLIENT, and the reconciler passes the pool', () => {
|
|
116
|
-
|
|
116
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
117
|
+
const db = lifecycleDbSource();
|
|
117
118
|
// db.js is over the fitness oversize threshold, so the hook is ONE line there
|
|
118
119
|
// and its body lives in done-when.closeCompletedWorkForShip. What must hold is
|
|
119
120
|
// that the thing handed in is the txn client, not a pool.
|
|
@@ -13,7 +13,7 @@ import { strict as assert } from 'node:assert';
|
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
|
-
import { makeRunner } from './helpers.mjs';
|
|
16
|
+
import { makeRunner, lifecycleDbSource } from './helpers.mjs';
|
|
17
17
|
|
|
18
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
const repoRoot = join(__dirname, '..');
|
|
@@ -22,7 +22,8 @@ const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
|
22
22
|
const mig = read('migrations', '163_polymorphic_dependencies.sql');
|
|
23
23
|
const migN = mig.replace(/\s+/g, ' ');
|
|
24
24
|
// the task-dependency CRUD + the claim-gate slice carved to the lifecycle module at BV1.R86.
|
|
25
|
-
|
|
25
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
26
|
+
const dbSrc = lifecycleDbSource();
|
|
26
27
|
|
|
27
28
|
const { test, summary } = makeRunner();
|
|
28
29
|
|
package/tests/fitness.mjs
CHANGED
|
@@ -542,6 +542,49 @@ test('identity gate skips comments + instance-injected tokens (no false positive
|
|
|
542
542
|
assert.equal(scanPromptIdentity('x.html', '<title>{{worldName}} — the gate</title>').length, 0, 'a {{worldName}} token carries no literal');
|
|
543
543
|
});
|
|
544
544
|
|
|
545
|
+
// task 1002756 — the two holes that let migrate.sh's founder DB default survive:
|
|
546
|
+
// (1) the stripper could be OPENED by a `/*` inside a `//` line comment (a real
|
|
547
|
+
// `*/` hundreds of lines later then swallowed everything between — provision.js
|
|
548
|
+
// lost ~330 lines from the scanned text), and (2) whole file classes were never
|
|
549
|
+
// scanned. These lock the stripper; the root widening is pinned by the note test.
|
|
550
|
+
test('identity gate: a /* inside a line comment cannot open a phantom block (task 1002756)', () => {
|
|
551
|
+
const { scanPromptIdentity } = fitness;
|
|
552
|
+
const src = [
|
|
553
|
+
'// glob note: import <dir>/*.caddy', // the phantom opener shape
|
|
554
|
+
'const leak = "amazonprimea.example";', // must STILL be seen
|
|
555
|
+
'doThing();',
|
|
556
|
+
'/* a real block */ const ok = 1;',
|
|
557
|
+
].join('\n');
|
|
558
|
+
assert.equal(scanPromptIdentity('x.js', src).length, 1, 'the literal after a /*-bearing line comment must be caught');
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('identity gate: a // inside a block comment does not truncate the block', () => {
|
|
562
|
+
const { scanPromptIdentity } = fitness;
|
|
563
|
+
const src = [
|
|
564
|
+
'/* rationale: see https://amazonprimea.com — historical // note',
|
|
565
|
+
'still inside the block, amazonprimea again',
|
|
566
|
+
'*/',
|
|
567
|
+
'const clean = 1;',
|
|
568
|
+
].join('\n');
|
|
569
|
+
assert.equal(scanPromptIdentity('x.js', src).length, 0, 'comment prose naming the brand must not trip the scan');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('identity gate: comment markers inside strings and regex literals are inert', () => {
|
|
573
|
+
const { scanPromptIdentity } = fitness;
|
|
574
|
+
// A string containing /* must not open a comment that hides a later literal…
|
|
575
|
+
assert.equal(scanPromptIdentity('x.js', 'const g = "src/*.js";\nconst leak = "amazonprimea";').length, 1);
|
|
576
|
+
// …a regex containing the brand is code and IS caught (the detectors carve out their own hits)…
|
|
577
|
+
assert.equal(scanPromptIdentity('x.js', 'const re = /amazonprimea/i;').length, 1);
|
|
578
|
+
// …and a division sign is not mistaken for a regex opener that swallows code.
|
|
579
|
+
assert.equal(scanPromptIdentity('x.js', 'const x = a / b / c;\nconst leak = "amazonprimea";').length, 1);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('identity gate scans the WHOLE scripts/ root, not just scripts/gds (task 1002756)', () => {
|
|
583
|
+
const r = fitness.checkKernelPromptsNoHardcodedIdentity();
|
|
584
|
+
assert.match(r.note, /scripts\/\*\* \(whole root/, 'note must document the widened root');
|
|
585
|
+
assert.equal(r.hardFail, false, 'live tree must be clean under the widened scan:\n' + r.violations.join('\n'));
|
|
586
|
+
});
|
|
587
|
+
|
|
545
588
|
// --- write-route validation build check (BV1.R12 / task 1999, ADR 0118) ---
|
|
546
589
|
|
|
547
590
|
test('write-route validation check: the live tree is clean (R11 sweep reached 100%)', () => {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import { strict as assert } from 'node:assert';
|
|
47
47
|
import { createRequire } from 'node:module';
|
|
48
48
|
import { readFileSync } from 'node:fs';
|
|
49
|
-
import { makeRunner } from './helpers.mjs';
|
|
49
|
+
import { makeRunner, lifecycleDbSource } from './helpers.mjs';
|
|
50
50
|
|
|
51
51
|
process.env.NODE_ENV = 'test';
|
|
52
52
|
|
|
@@ -201,7 +201,8 @@ await test('[Q6] non-member on a public PROTECTED-scope goal → action=apply (s
|
|
|
201
201
|
assert.equal(r.action, 'apply', 'a protected-scope goal is never offered as open-join');
|
|
202
202
|
});
|
|
203
203
|
await test('[Q7] source contract: BOTH INSERT INTO claims sites run the extracted gate (batch cannot bypass)', () => {
|
|
204
|
-
|
|
204
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
205
|
+
const dbSrc = lifecycleDbSource();
|
|
205
206
|
const insertSites = dbSrc.match(/INSERT INTO claims \(/g) || [];
|
|
206
207
|
assert.equal(insertSites.length, 2, 'exactly two claim-insert sites (claimTask + claimTasksBatch) — a third needs the gate too');
|
|
207
208
|
const gateCalls = dbSrc.match(/await goalMembershipFailures\(/g) || [];
|
package/tests/goal_advisory.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { strict as assert } from 'node:assert';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { readFileSync } from 'node:fs';
|
|
19
19
|
import http from 'node:http';
|
|
20
|
-
import { makeRunner } from './helpers.mjs';
|
|
20
|
+
import { makeRunner, lifecycleDbSource } from './helpers.mjs';
|
|
21
21
|
|
|
22
22
|
process.env.NODE_ENV = 'test';
|
|
23
23
|
|
|
@@ -25,7 +25,8 @@ const require = createRequire(import.meta.url);
|
|
|
25
25
|
const { adviseGoal, candidateGoals, generalGoalTitle, buildGoalAdvisory } = require('../modules/lifecycle/goal-advisory.js');
|
|
26
26
|
|
|
27
27
|
const tasksSrc = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
|
|
28
|
-
|
|
28
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
29
|
+
const dbSrc = lifecycleDbSource();
|
|
29
30
|
const { test, summary } = makeRunner();
|
|
30
31
|
|
|
31
32
|
const V = 'BONGOS-V1';
|
|
@@ -37,6 +37,7 @@ const LIFECYCLE = path.join(ROOT, 'modules', 'lifecycle');
|
|
|
37
37
|
const eligibility = require(path.join(LIFECYCLE, 'claim-eligibility.js'));
|
|
38
38
|
const db = require(path.join(LIFECYCLE, 'db.js'));
|
|
39
39
|
const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
40
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
40
41
|
|
|
41
42
|
const { CLAIM_ANY, CLAIM_NEWCOMER, permissionAllowsClaim, claimEligibilityAllows } = eligibility;
|
|
42
43
|
|
|
@@ -147,7 +148,8 @@ test('the permission predicate cannot see requires_rank at all (structural)', ()
|
|
|
147
148
|
});
|
|
148
149
|
|
|
149
150
|
test('every claim path in db.js still gates on requires_rank as its own check', () => {
|
|
150
|
-
|
|
151
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
152
|
+
const src = lifecycleDbSource();
|
|
151
153
|
// claimTask, claimTasksBatch, validateClaimBatch — three call sites, three
|
|
152
154
|
// separate rankAllowsTask conjuncts. If a refactor ever folds the floor into the
|
|
153
155
|
// permission predicate, this count drops and the test reds.
|
|
@@ -18,6 +18,7 @@ import { createRequire } from 'node:module';
|
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
21
22
|
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -189,8 +190,14 @@ test('the writer runs on the CALLER\'s transaction client when given one', () =>
|
|
|
189
190
|
});
|
|
190
191
|
|
|
191
192
|
test('both rank-change twins rotate the role inside their transaction', () => {
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
// task 1003204: the lifecycle twin moved out of db.js into db-rank-authz.js
|
|
194
|
+
// when the monolith was carved up. Read the whole lifecycle family so the
|
|
195
|
+
// assertion follows the code instead of a filename.
|
|
196
|
+
const twins = [
|
|
197
|
+
['src/bongos/db.js', fs.readFileSync(path.join(ROOT, 'src/bongos/db.js'), 'utf8')],
|
|
198
|
+
['modules/lifecycle/db-*.js', lifecycleDbSource()],
|
|
199
|
+
];
|
|
200
|
+
for (const [rel, src] of twins) {
|
|
194
201
|
const body = src.slice(src.indexOf('async function _applyRankChangeTx'));
|
|
195
202
|
const upd = body.indexOf('UPDATE builders SET rank');
|
|
196
203
|
const sync = body.indexOf('await syncGovernanceRankRole(client');
|
package/tests/helpers.mjs
CHANGED
|
@@ -123,3 +123,22 @@ export function requireHostModule(require, spec, note) {
|
|
|
123
123
|
throw err;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
// lifecycleDbSource — the LIFECYCLE data layer as one source string.
|
|
128
|
+
//
|
|
129
|
+
// task 1003204 (goal 1000079) split the 5,748-line modules/lifecycle/db.js into
|
|
130
|
+
// db.js (a pure re-export facade) + twelve cohesive db-*.js units. A pile of
|
|
131
|
+
// tests assert things about "the data layer" by regexing its source — that the
|
|
132
|
+
// claim paths gate on requires_rank, that a literal appears in exactly one
|
|
133
|
+
// place, that dependency CRUD points at the right table. Those assertions were
|
|
134
|
+
// always about the LAYER, not about one filename, so they read the whole family
|
|
135
|
+
// here. Reading the family (rather than repointing each test at whichever db-*
|
|
136
|
+
// unit happens to hold the code today) also means a future re-carve moves code
|
|
137
|
+
// between units without silently disarming the assertion.
|
|
138
|
+
export function lifecycleDbSource(modulesDir = path.join(ROOT, 'modules')) {
|
|
139
|
+
const dir = path.join(modulesDir, 'lifecycle');
|
|
140
|
+
const files = fs.readdirSync(dir)
|
|
141
|
+
.filter((f) => f === 'db.js' || (f.startsWith('db-') && f.endsWith('.js')))
|
|
142
|
+
.sort();
|
|
143
|
+
return files.map((f) => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n');
|
|
144
|
+
}
|
|
@@ -28,6 +28,7 @@ import { readFileSync } from 'node:fs';
|
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
29
|
import { dirname, join } from 'node:path';
|
|
30
30
|
import { createRequire } from 'node:module';
|
|
31
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
31
32
|
|
|
32
33
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
34
|
const repoRoot = join(__dirname, '..');
|
|
@@ -49,7 +50,8 @@ async function test(name, fn) {
|
|
|
49
50
|
const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
50
51
|
const migrationSql = read('migrations', '081_tasks_needs_migration.sql');
|
|
51
52
|
// the tasks db slice + the tasks route carved to modules/lifecycle/ at BV1.R86.
|
|
52
|
-
|
|
53
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
54
|
+
const dbSrc = lifecycleDbSource();
|
|
53
55
|
const routesSrc = read('modules', 'lifecycle', 'routes', 'tasks.js');
|
|
54
56
|
const captureSrc = read('scripts', 'gds', 'capture.js');
|
|
55
57
|
|
|
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const reconciler = require('../modules/lifecycle/publish-reconciler.js');
|
|
19
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
19
20
|
|
|
20
21
|
let passed = 0;
|
|
21
22
|
let failed = 0;
|
|
@@ -1647,7 +1648,8 @@ test('vector 2: shipped_at is stamped ONLY by shipTask, and shipTask requires co
|
|
|
1647
1648
|
const url = await import('node:url');
|
|
1648
1649
|
const path = await import('node:path');
|
|
1649
1650
|
const ROOT = path.dirname(path.dirname(url.fileURLToPath(import.meta.url)));
|
|
1650
|
-
|
|
1651
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
1652
|
+
const dbSrc = lifecycleDbSource();
|
|
1651
1653
|
|
|
1652
1654
|
// Every statement that writes shipped_at, ignoring SELECT projections and comments.
|
|
1653
1655
|
const writes = [...dbSrc.matchAll(/shipped_at\s*=\s*now\(\)/g)];
|
|
@@ -1693,3 +1695,41 @@ setTimeout(() => {
|
|
|
1693
1695
|
console.log(`\npublish reconciler (task 1134 + 1915) tests: ${passed} passed, ${failed} failed`);
|
|
1694
1696
|
process.exit(failed > 0 ? 1 : 0);
|
|
1695
1697
|
}, 100);
|
|
1698
|
+
|
|
1699
|
+
// --- task 1003217: a sweep that only RAISED rows must not log silence --------
|
|
1700
|
+
|
|
1701
|
+
test('1003217 runCostSweep reports raised rows (a raise-only sweep still logs)', async () => {
|
|
1702
|
+
const logs = [];
|
|
1703
|
+
const deps = {
|
|
1704
|
+
db: { reconcileSessionCosts: async () => ({ scanned: 12, reconciled: 0, raised: 3, totalUsd: 45, unknownModels: [] }) },
|
|
1705
|
+
log: (m) => logs.push(m),
|
|
1706
|
+
error: () => {},
|
|
1707
|
+
};
|
|
1708
|
+
const r = await reconciler.runCostSweep(deps);
|
|
1709
|
+
assert.equal(r.raised, 3, 'the raised count passes through');
|
|
1710
|
+
assert.equal(logs.length, 1, 'a sweep that raised 3 rows logged — the old guard was reconciled>0 only');
|
|
1711
|
+
assert.match(logs[0], /raised 3/, 'and the raise is named in the line');
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
test('1003217 a sweep that did nothing at all still logs nothing', async () => {
|
|
1715
|
+
const logs = [];
|
|
1716
|
+
const deps = {
|
|
1717
|
+
db: { reconcileSessionCosts: async () => ({ scanned: 12, reconciled: 0, raised: 0, totalUsd: 0, unknownModels: [] }) },
|
|
1718
|
+
log: (m) => logs.push(m),
|
|
1719
|
+
error: () => {},
|
|
1720
|
+
};
|
|
1721
|
+
await reconciler.runCostSweep(deps);
|
|
1722
|
+
assert.equal(logs.length, 0, 'no ledger work → no line, as before');
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
test('1003217 an older deps bundle with no `raised` key still logs on reconciled', async () => {
|
|
1726
|
+
const logs = [];
|
|
1727
|
+
const deps = {
|
|
1728
|
+
db: { reconcileSessionCosts: async () => ({ scanned: 2, reconciled: 2, totalUsd: 3, unknownModels: [] }) },
|
|
1729
|
+
log: (m) => logs.push(m),
|
|
1730
|
+
error: () => {},
|
|
1731
|
+
};
|
|
1732
|
+
await reconciler.runCostSweep(deps);
|
|
1733
|
+
assert.equal(logs.length, 1);
|
|
1734
|
+
assert.doesNotMatch(logs[0], /raised/, 'no phantom "raised 0" noise when the key is absent');
|
|
1735
|
+
});
|
package/tests/rank_gate.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const require = createRequire(import.meta.url);
|
|
|
26
26
|
const auth = require('../src/bongos/auth.js');
|
|
27
27
|
const db = require('../modules/lifecycle/db.js');
|
|
28
28
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
29
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
29
30
|
|
|
30
31
|
let passed = 0;
|
|
31
32
|
let failed = 0;
|
|
@@ -319,7 +320,8 @@ console.log('\nlistClaimableTasks SQL rank-tier CASE (regression: thetes drift):
|
|
|
319
320
|
|
|
320
321
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
321
322
|
// listClaimableTasks carved to the lifecycle module at BV1.R86 — read the SQL there.
|
|
322
|
-
|
|
323
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
324
|
+
const dbSrc = lifecycleDbSource();
|
|
323
325
|
|
|
324
326
|
t('every live rank appears in the claimable CASE at its REQUIRES_RANK_TIER value', () => {
|
|
325
327
|
// Isolate the listClaimableTasks function body so we don't match CASE
|
package/tests/reward_gate.mjs
CHANGED
|
@@ -25,6 +25,7 @@ const db = require('../modules/lifecycle/db.js');
|
|
|
25
25
|
// the SAME singleton object db.js captured, so mutating its .query stubs the INSERT
|
|
26
26
|
// createTask runs — no database needed.
|
|
27
27
|
const sharedPool = require('../src/module-api.js').pool;
|
|
28
|
+
import { lifecycleDbSource } from './helpers.mjs';
|
|
28
29
|
|
|
29
30
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
const root = join(__dir, '..');
|
|
@@ -132,7 +133,8 @@ t('null task is a no-op (no throw, no assignment)', () => {
|
|
|
132
133
|
console.log('\nenforcement points (source-wiring — AUTO-ASSIGN):');
|
|
133
134
|
|
|
134
135
|
const tasksRouteSrc = readFileSync(join(root, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
135
|
-
|
|
136
|
+
// task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
|
|
137
|
+
const dbSrc = lifecycleDbSource();
|
|
136
138
|
const migration178 = readFileSync(join(root, 'migrations/178_reward_gate_auto_assign_revert_autopromote.sql'), 'utf8');
|
|
137
139
|
|
|
138
140
|
t('promote route AUTO-ASSIGNS then proceeds (no 409 refusal for missing reward)', () => {
|
package/tests/session_cost.mjs
CHANGED
|
@@ -131,3 +131,67 @@ test('1344 addComponents / addTiers / roundTiers are pure and additive', () => {
|
|
|
131
131
|
const rounded = sc.roundTiers({ fresh_input_usd: 1.000049, cache_read_usd: 0, cache_write_usd: 0, output_usd: 0 });
|
|
132
132
|
assert.equal(rounded.fresh_input_usd, 1.0); // round to 4dp
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
// ---------- task 1003217: price one API call once, not once per line ----------
|
|
136
|
+
//
|
|
137
|
+
// A single assistant message is written to the transcript as several lines (one
|
|
138
|
+
// per content block: thinking / text / tool_use), each repeating the SAME
|
|
139
|
+
// message.usage. priceTranscript must dedup on message.id. Measured 2.22x
|
|
140
|
+
// over-pricing on 12 real transcripts before this fix — and ADR 0054 pays
|
|
141
|
+
// round(true_cost_usd x 1.20) off this number.
|
|
142
|
+
function splitLine(id, model, usage, block) {
|
|
143
|
+
return JSON.stringify({ message: { id, role: 'assistant', model, usage, content: [block] } });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test('1003217 priceTranscript charges a split assistant message ONCE', () => {
|
|
147
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-dedup-'));
|
|
148
|
+
const p = path.join(dir, 'split.jsonl');
|
|
149
|
+
const usage = { output_tokens: M }; // opus-5 output = $25/M
|
|
150
|
+
fs.writeFileSync(p, [
|
|
151
|
+
splitLine('msg_1', 'claude-opus-5', usage, { type: 'thinking', thinking: 'hmm' }),
|
|
152
|
+
splitLine('msg_1', 'claude-opus-5', usage, { type: 'text', text: 'ok' }),
|
|
153
|
+
splitLine('msg_1', 'claude-opus-5', usage, { type: 'tool_use', name: 'Bash' }),
|
|
154
|
+
].join('\n'));
|
|
155
|
+
const r = sc.priceTranscript(p);
|
|
156
|
+
assert.equal(r.total, 25, 'one call at 1M opus output = $25, NOT $75');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('1003217 priceTranscript still charges distinct message ids separately', () => {
|
|
160
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-dedup2-'));
|
|
161
|
+
const p = path.join(dir, 'three.jsonl');
|
|
162
|
+
const usage = { output_tokens: M };
|
|
163
|
+
fs.writeFileSync(p, [
|
|
164
|
+
splitLine('msg_1', 'claude-opus-5', usage, { type: 'text', text: 'a' }),
|
|
165
|
+
splitLine('msg_2', 'claude-opus-5', usage, { type: 'text', text: 'b' }),
|
|
166
|
+
splitLine('msg_3', 'claude-opus-5', usage, { type: 'text', text: 'c' }),
|
|
167
|
+
].join('\n'));
|
|
168
|
+
assert.equal(sc.priceTranscript(p).total, 75, 'three real calls = $75');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('1003217 priceTranscript keeps the tier split consistent with the deduped total', () => {
|
|
172
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-dedup3-'));
|
|
173
|
+
const p = path.join(dir, 'tiers.jsonl');
|
|
174
|
+
const usage = { output_tokens: M, cache_read_input_tokens: M };
|
|
175
|
+
fs.writeFileSync(p, [
|
|
176
|
+
splitLine('msg_1', 'claude-opus-5', usage, { type: 'text', text: 'a' }),
|
|
177
|
+
splitLine('msg_1', 'claude-opus-5', usage, { type: 'tool_use', name: 'Bash' }),
|
|
178
|
+
].join('\n'));
|
|
179
|
+
const r = sc.priceTranscript(p);
|
|
180
|
+
// opus-5: output $25/M, cache_read $0.50/M → $25.50 for ONE call.
|
|
181
|
+
assert.equal(r.total, 25.5);
|
|
182
|
+
assert.equal(r.tiers.output_usd, 25);
|
|
183
|
+
assert.equal(r.tiers.cache_read_usd, 0.5);
|
|
184
|
+
const tierSum = r.tiers.fresh_input_usd + r.tiers.cache_read_usd
|
|
185
|
+
+ r.tiers.cache_write_usd + r.tiers.output_usd;
|
|
186
|
+
assert.ok(Math.abs(tierSum - r.total) < 1e-9, 'the four tiers must still sum to total');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('1003217 a transcript with no message.id prices per line (unchanged legacy path)', () => {
|
|
190
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-dedup4-'));
|
|
191
|
+
const p = path.join(dir, 'noid.jsonl');
|
|
192
|
+
fs.writeFileSync(p, [
|
|
193
|
+
JSON.stringify({ message: { role: 'assistant', model: 'claude-opus-5', usage: { output_tokens: M } } }),
|
|
194
|
+
JSON.stringify({ message: { role: 'assistant', model: 'claude-opus-5', usage: { output_tokens: M } } }),
|
|
195
|
+
].join('\n'));
|
|
196
|
+
assert.equal(sc.priceTranscript(p).total, 50, 'no id to dedup on → both lines charged');
|
|
197
|
+
});
|