| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const { runTihaoSourcing } = require('../mcp/src/features/tihao-sourcing/sourcing-workflow');
- const { redactSecrets } = require('../mcp/src/core/result-envelope');
- const STATUS_STRONG = '\u5f3a\u63a8\u8350';
- const STATUS_BACKUP = '\u5907\u9009';
- const STATUS_REVIEW = '\u9700\u590d\u6838';
- const STATUS_EXCLUDED = '\u5df2\u5254\u9664';
- async function main() {
- const root = path.resolve(__dirname, '..');
- const fixturesDir = path.resolve(process.env.TIHAO_QUALITY_FIXTURES || path.join(root, 'fixtures', 'quality-briefs'));
- const fixtures = loadFixtures(fixturesDir);
- if (!fixtures.length) throw new Error(`No quality fixtures found in ${fixturesDir}`);
- const credentials = readCredentials();
- const liveEnabled = process.env.TIHAO_OVERNIGHT_LIVE === 'false'
- ? false
- : process.env.TIHAO_OVERNIGHT_LIVE === 'true' || Boolean(credentials.runtimeCredential && credentials.company);
- const outputRoot = path.resolve(process.env.TIHAO_OVERNIGHT_OUTPUT || path.join(root, 'outputs', `overnight-quality-${Date.now()}`));
- fs.mkdirSync(outputRoot, { recursive: true });
- const selectedFixtureIds = csvSet(process.env.TIHAO_OVERNIGHT_FIXTURES);
- const selectedVariantIds = csvSet(process.env.TIHAO_OVERNIGHT_VARIANTS);
- const activeFixtures = selectedFixtureIds.size ? fixtures.filter(item => selectedFixtureIds.has(item.id)) : fixtures;
- const variants = buildVariants({ liveEnabled, credentials }).filter(item => !selectedVariantIds.size || selectedVariantIds.has(item.id));
- const options = readRunOptions({ liveEnabled, credentials });
- const manifest = {
- startedAt: new Date().toISOString(),
- fixturesDir,
- outputRoot,
- liveEnabled,
- videoAvailable: Boolean(liveEnabled && credentials.vocSocialToken && credentials.videoToken),
- fixtureCount: activeFixtures.length,
- variants: variants.map(item => ({ id: item.id, description: item.description, gateRole: item.gateRole || 'release' })),
- options: publicRunOptions(options)
- };
- writeJson(path.join(outputRoot, 'manifest.json'), manifest);
- const runs = [];
- const failures = [];
- let attemptedRuns = 0;
- for (const fixture of activeFixtures) {
- for (const variant of variants) {
- if (options.maxRuns && attemptedRuns >= options.maxRuns) {
- process.stderr.write(`overnight-quality: max runs reached (${options.maxRuns})\n`);
- break;
- }
- const runDir = path.join(outputRoot, 'runs', fixture.id, variant.id);
- fs.mkdirSync(runDir, { recursive: true });
- const summaryFile = path.join(runDir, 'quality-summary.json');
- if (options.resume && fs.existsSync(summaryFile)) {
- process.stderr.write(`overnight-quality: resume ${fixture.id}/${variant.id}\n`);
- runs.push({ ...JSON.parse(fs.readFileSync(summaryFile, 'utf8')), gateRole: variant.gateRole || 'release' });
- continue;
- }
- process.stderr.write(`overnight-quality: ${fixture.id}/${variant.id}\n`);
- attemptedRuns += 1;
- try {
- const startedAt = Date.now();
- const input = buildRunInput({ fixture, variant, runDir, credentials });
- const result = await runSourcingWithRetries(input, options.runRetries);
- const summary = summarizeRun({
- fixture,
- variant,
- result,
- runDir,
- durationMs: Date.now() - startedAt
- });
- runs.push(summary);
- writeJson(summaryFile, summary);
- } catch (error) {
- const failure = {
- fixtureId: fixture.id,
- variant: variant.id,
- message: redactSecrets(error && error.stack ? error.stack : String(error))
- };
- failures.push(failure);
- writeJson(path.join(runDir, 'failure.json'), failure);
- }
- if (options.delayMs > 0) await sleep(options.delayMs);
- }
- if (options.maxRuns && attemptedRuns >= options.maxRuns) break;
- }
- const aggregate = buildAggregate({ manifest, runs, failures, fixtures: activeFixtures, variants, options });
- writeJson(path.join(outputRoot, 'aggregate-summary.json'), aggregate);
- writeJson(path.join(outputRoot, 'failures.json'), failures);
- writeText(path.join(outputRoot, 'aggregate-report.md'), renderAggregateReportClean(aggregate));
- writeText(path.join(outputRoot, 'manual-review-sample.csv'), buildManualReviewSample(runs));
- scanForLeaks(outputRoot, credentials.secrets);
- if (options.failOnGates && !aggregate.acceptance.overallPass) {
- process.exitCode = 2;
- }
- console.log(redactSecrets(JSON.stringify({
- outputRoot,
- liveEnabled,
- fixtures: activeFixtures.length,
- variants: variants.length,
- runs: runs.length,
- failures: failures.length,
- gatePass: aggregate.acceptance.overallPass,
- aggregateReport: path.join(outputRoot, 'aggregate-report.md')
- }, null, 2)));
- }
- function readCredentials() {
- const runtimeCredential = process.env.TIHAO_SESSION_TOKEN || process.env.VOC_ECOMMERCE_TOKEN || process.env.VOC_TOKEN || '';
- const company = process.env.TIHAO_COMPANY || process.env.VOC_ECOMMERCE_COMPANY || process.env.COMPANY_OBJECT_ID || '';
- const vocSocialToken = process.env.VOC_SOCIAL_TOKEN || runtimeCredential;
- const videoToken = process.env.VIDEO_ANALYSIS_TOKEN || process.env.DOUBAO_VISION_TOKEN || '';
- return {
- runtimeCredential,
- company,
- vocSocialToken,
- videoToken,
- videoBaseUrl: process.env.VIDEO_ANALYSIS_BASE_URL || process.env.DOUBAO_VISION_BASE_URL || 'https://api.fmode.cn',
- videoModel: process.env.VIDEO_ANALYSIS_MODEL || process.env.DOUBAO_VISION_MODEL || 'doubao-seed-2-0-pro',
- secrets: [runtimeCredential, vocSocialToken, videoToken].filter(Boolean)
- };
- }
- function readRunOptions({ liveEnabled, credentials }) {
- const videoAvailable = Boolean(liveEnabled && credentials.vocSocialToken && credentials.videoToken);
- return {
- maxRuns: intEnv('TIHAO_OVERNIGHT_MAX_RUNS', 0),
- delayMs: intEnv('TIHAO_OVERNIGHT_DELAY_MS', liveEnabled ? 1500 : 0),
- runRetries: intEnv('TIHAO_OVERNIGHT_RUN_RETRIES', liveEnabled ? 1 : 0),
- resume: process.env.TIHAO_OVERNIGHT_RESUME === 'true',
- failOnGates: process.env.TIHAO_OVERNIGHT_FAIL_ON_GATES === 'true',
- gates: {
- minEvidenceCoverage: numberEnv('TIHAO_GATE_MIN_EVIDENCE_COVERAGE', videoAvailable ? 0.8 : 0),
- maxNegativeRiskRate: numberEnv('TIHAO_GATE_MAX_NEGATIVE_RISK_RATE', numberEnv('TIHAO_GATE_MAX_OFF_TOPIC_RATE', 0.1)),
- maxScoreDrop: numberEnv('TIHAO_GATE_MAX_SCORE_DROP', liveEnabled ? 1 : 999),
- requireLiveRecall: process.env.TIHAO_GATE_REQUIRE_LIVE_RECALL === 'false' ? false : liveEnabled,
- requireReferenceProvider: process.env.TIHAO_GATE_REQUIRE_REFERENCE_PROVIDER === 'true',
- requireHomepageProvider: process.env.TIHAO_GATE_REQUIRE_HOMEPAGE_PROVIDER === 'true',
- requireResultFirstNoStrongDrop: process.env.TIHAO_GATE_REQUIRE_NO_STRONG_DROP === 'false' ? false : true
- }
- };
- }
- function publicRunOptions(options) {
- return {
- maxRuns: options.maxRuns,
- delayMs: options.delayMs,
- runRetries: options.runRetries,
- resume: options.resume,
- failOnGates: options.failOnGates,
- gates: options.gates
- };
- }
- async function runSourcingWithRetries(input, retries) {
- let lastResult = null;
- for (let attempt = 0; attempt <= retries; attempt++) {
- lastResult = await runTihaoSourcing(input);
- if (!shouldRetryRun(lastResult) || attempt === retries) return lastResult;
- await sleep(1000 * (attempt + 1));
- }
- return lastResult;
- }
- function shouldRetryRun(result) {
- return result && ['live_network_error'].includes(result.status);
- }
- function buildVariants({ liveEnabled, credentials }) {
- const mode = liveEnabled ? 'live' : 'sample';
- const canUseVideo = Boolean(liveEnabled && credentials.vocSocialToken && credentials.videoToken);
- const canUseReference = Boolean(liveEnabled && credentials.vocSocialToken);
- return [
- {
- id: 'baseline-live',
- gateRole: 'baseline',
- description: liveEnabled ? 'Brief-only low-volume live baseline' : 'Sample-mode baseline',
- input: { collectionMode: mode, keywordLimit: 1, pagesPerKeyword: 1 }
- },
- {
- id: 'reference-account',
- gateRole: 'release',
- description: 'Reference account/link enrichment and similarity-aware recall',
- input: {
- collectionMode: mode,
- keywordLimit: liveEnabled ? 2 : 1,
- pagesPerKeyword: 1,
- enableReferenceEnrichment: true,
- enableVocSocialReferenceEnrichment: canUseReference,
- useVocSocialReferenceEnrichment: canUseReference,
- referencePostsLimit: 20
- }
- },
- {
- id: 'homepage-evidence',
- gateRole: 'release',
- description: 'Recent homepage content evidence and quality scoring',
- input: {
- collectionMode: mode,
- keywordLimit: liveEnabled ? 2 : 1,
- pagesPerKeyword: 1,
- homepageEvidenceLimit: 20,
- recentContentsLimit: 20,
- homepageEvidenceCreatorsLimit: liveEnabled ? 20 : 8
- }
- },
- {
- id: 'video-enhanced',
- gateRole: 'diagnostic',
- description: 'Reference video enrichment with bounded evidence analysis',
- input: {
- collectionMode: mode,
- keywordLimit: liveEnabled ? 2 : 1,
- pagesPerKeyword: 1,
- evidenceCreatorsLimit: liveEnabled ? 10 : 6,
- resultFirstMode: liveEnabled,
- optimizeForResults: liveEnabled,
- enableVocSocialReferenceEnrichment: canUseVideo,
- videoEnabled: canUseVideo
- }
- },
- {
- id: 'result-first',
- gateRole: 'release',
- description: 'Quality-first broad recall and batched evidence',
- input: {
- collectionMode: mode,
- keywordLimit: liveEnabled ? 2 : 1,
- pagesPerKeyword: 1,
- evidenceCreatorsLimit: liveEnabled ? 10 : 6,
- resultFirstMode: liveEnabled,
- optimizeForResults: liveEnabled,
- enableVocSocialReferenceEnrichment: canUseVideo,
- videoEnabled: canUseVideo
- }
- },
- {
- id: 'result-first-risk',
- gateRole: 'diagnostic',
- description: 'Quality-first mode with evidence risk visibility',
- input: {
- collectionMode: mode,
- keywordLimit: liveEnabled ? 2 : 1,
- pagesPerKeyword: 1,
- evidenceCreatorsLimit: liveEnabled ? 10 : 6,
- resultFirstMode: liveEnabled,
- optimizeForResults: liveEnabled,
- evidenceBatchSize: 5,
- enableVocSocialReferenceEnrichment: canUseVideo,
- videoEnabled: canUseVideo
- }
- },
- {
- id: 'result-first-broad',
- gateRole: 'release',
- description: 'Quality-first mode with broader live recall',
- input: {
- collectionMode: mode,
- keywordLimit: liveEnabled ? 4 : 1,
- pagesPerKeyword: liveEnabled ? 2 : 1,
- resultFirstMode: liveEnabled,
- optimizeForResults: liveEnabled,
- evidenceBatchSize: 5,
- enableVocSocialReferenceEnrichment: canUseVideo,
- videoEnabled: canUseVideo
- }
- }
- ];
- }
- function buildRunInput({ fixture, variant, runDir, credentials }) {
- const input = {
- ...variant.input,
- ...(fixture.input || {}),
- briefText: fixture.briefText || '',
- referenceLinks: fixture.referenceLinks || [],
- targetCount: fixture.expected?.targetCount,
- output: runDir
- };
- if (variant.input.collectionMode === 'live') {
- input.tihaoToken = credentials.runtimeCredential;
- input.company = credentials.company;
- }
- if (variant.input.videoEnabled) {
- input.vocSocialToken = credentials.vocSocialToken;
- input.videoAnalysisBaseUrl = credentials.videoBaseUrl;
- input.videoAnalysisModel = credentials.videoModel;
- input.videoAnalysisToken = credentials.videoToken;
- }
- delete input.videoEnabled;
- return input;
- }
- function summarizeRun({ fixture, variant, result, runDir, durationMs }) {
- const data = result.data || {};
- const criteria = data.criteria || {};
- const candidates = data.candidates || [];
- const top10 = candidates.slice(0, 10);
- const recallRecords = criteria.recallRecords || [];
- const offTopic = top10.filter(item => hasOffTopicHit(item, fixture.expected?.offTopicTerms || []));
- const negativeRiskRate = ratio(offTopic.length, Math.max(top10.length, 1));
- const evidenceHitCount = top10.filter(item => item.evidenceCard || (item.evidenceSignals || []).length).length;
- const softwareTable = inspectSoftwareTable(path.join(runDir, 'tihao-sourcing-client-list.csv'));
- return {
- briefId: fixture.id,
- briefName: fixture.name,
- variant: variant.id,
- gateRole: variant.gateRole || 'release',
- status: result.status,
- durationMs,
- recallRequestCount: recallRecords.length,
- liveRecallCount: recallRecords.reduce((sum, item) => sum + Number(item.normalizedCount || 0), 0),
- uniqueCandidateCount: candidates.length,
- clientListCount: candidates.length,
- strongCount: countStatus(candidates, STATUS_STRONG),
- backupCount: countStatus(candidates, STATUS_BACKUP),
- reviewCount: countStatus(candidates, STATUS_REVIEW),
- excludedCount: countStatus(candidates, STATUS_EXCLUDED),
- top10EvidenceCoverage: ratio(evidenceHitCount, Math.max(top10.length, 1)),
- avgTop10BriefFitScore: avg(top10.map(item => item.briefFitScore)),
- avgTop10ReferenceStyleFitScore: avg(top10.map(item => item.referenceStyleFitScore || item.referenceSimilarity)),
- avgTop10TotalScore: avg(top10.map(item => item.score)),
- avgEvidenceBoost: avg(top10.map(item => item.evidenceScoreBoost)),
- avgEvidenceRiskPenalty: avg(top10.map(item => item.evidenceRiskPenalty)),
- offTopicRate: negativeRiskRate,
- negativeRiskRate,
- missingKeyConditionRate: ratio(top10.filter(item => (item.missingKeyConditions || []).length).length, Math.max(top10.length, 1)),
- softwareTable,
- providerStatus: {
- reference: criteria.referenceEvidenceStatus || {},
- fingerprint: criteria.referenceFingerprintStatus || {},
- evidence: criteria.evidenceStatus || {},
- homepage: criteria.homepageEvidenceStatus || {}
- },
- top10: top10.map(item => ({
- rank: item.rank,
- platform: item.platform,
- displayName: item.displayName,
- recommendStatus: item.recommendStatus,
- score: item.score,
- briefFitScore: item.briefFitScore,
- referenceStyleFitScore: item.referenceStyleFitScore || item.referenceSimilarity || 0,
- recentContentFitScore: item.recentContentFitScore || 0,
- visualQualityScore: item.visualQualityScore || 0,
- toneConsistencyScore: item.toneConsistencyScore || 0,
- evidenceScoreBoost: item.evidenceScoreBoost || 0,
- evidenceRiskPenalty: item.evidenceRiskPenalty || 0,
- homepageEvidenceHitPoints: (item.homepageEvidenceHitPoints || []).slice(0, 5),
- referenceStyleHitPoints: (item.referenceStyleHitPoints || []).slice(0, 5),
- referenceFallbackHitPoints: (item.referenceFallbackHitPoints || []).slice(0, 5),
- referenceEvidenceSource: item.referenceEvidenceSource || '',
- referenceEvidenceConcrete: Boolean(item.referenceEvidenceConcrete),
- reason: item.recommendReason,
- riskNote: item.riskNote,
- profileUrl: item.profileUrl || '',
- offTopicHits: findOffTopicHits(item, fixture.expected?.offTopicTerms || [])
- })),
- warnings: result.warnings || [],
- files: result.files || listRunFiles(runDir)
- };
- }
- function buildAggregate({ manifest, runs, failures, fixtures, variants, options }) {
- const byBrief = fixtures.map(fixture => {
- const fixtureRuns = runs.filter(item => item.briefId === fixture.id);
- const baseline = fixtureRuns.find(item => item.variant === 'baseline-live');
- const best = fixtureRuns.slice().sort((a, b) => qualityScore(b, baseline) - qualityScore(a, baseline))[0] || null;
- return {
- briefId: fixture.id,
- briefName: fixture.name,
- baseline,
- best,
- runs: fixtureRuns.map(item => ({
- variant: item.variant,
- gateRole: item.gateRole || 'release',
- status: item.status,
- strongCount: item.strongCount,
- uniqueCandidateCount: item.uniqueCandidateCount,
- liveRecallCount: item.liveRecallCount,
- top10EvidenceCoverage: item.top10EvidenceCoverage,
- avgTop10TotalScore: item.avgTop10TotalScore,
- avgTop10ReferenceStyleFitScore: item.avgTop10ReferenceStyleFitScore,
- offTopicRate: item.offTopicRate,
- negativeRiskRate: item.negativeRiskRate ?? item.offTopicRate,
- softwareDuplicateKeyCount: item.softwareTable?.duplicateKeyCount ?? 0,
- softwareRankContinuous: Boolean(item.softwareTable?.rankContinuous),
- referenceProviderStatus: item.providerStatus?.reference?.status || item.providerStatus?.reference?.providerStatus || 'missing',
- homepageProviderStatus: item.providerStatus?.homepage?.status || 'missing',
- gates: evaluateRunGates({ run: item, baseline, manifest, options })
- }))
- };
- });
- return {
- manifest,
- finishedAt: new Date().toISOString(),
- runCount: runs.length,
- failureCount: failures.length,
- fixtureCount: fixtures.length,
- variantCount: variants.length,
- byBrief,
- runs,
- failures,
- acceptance: evaluateAggregate({ byBrief, failures, manifest, options })
- };
- }
- function evaluateAggregate({ byBrief, failures, manifest, options }) {
- const comparable = byBrief.filter(item => item.baseline && item.best);
- const resultFirstWins = comparable.filter(item => item.best.variant !== 'baseline-live' && item.best.strongCount >= item.baseline.strongCount).length;
- const gates = byBrief.flatMap(brief => brief.runs.flatMap(run => run.gates || []));
- const failedGates = gates.filter(gate => gate.status === 'fail' && gate.gateRole !== 'diagnostic');
- const warningGates = gates.filter(gate => gate.status === 'warn');
- const diagnosticFailures = gates.filter(gate => gate.status === 'fail' && gate.gateRole === 'diagnostic');
- const releaseBriefs = byBrief.map(brief => {
- const releaseRuns = brief.runs.filter(run => run.gateRole === 'release');
- const bestRelease = releaseRuns.slice().sort((a, b) => runGateScore(b) - runGateScore(a))[0] || null;
- return {
- briefId: brief.briefId,
- bestReleaseVariant: bestRelease?.variant || '',
- bestReleasePass: bestRelease ? !(bestRelease.gates || []).some(gate => gate.status === 'fail') : false
- };
- });
- const releaseCoveragePass = releaseBriefs.every(item => item.bestReleasePass);
- return {
- noUnhandledFailures: failures.length === 0,
- comparableBriefs: comparable.length,
- resultFirstWinRate: ratio(resultFirstWins, Math.max(comparable.length, 1)),
- overallPass: failures.length === 0 && failedGates.length === 0 && releaseCoveragePass,
- releaseCoveragePass,
- releaseBriefs,
- failedGateCount: failedGates.length,
- warningGateCount: warningGates.length + diagnosticFailures.length,
- gates: {
- liveEnabled: manifest.liveEnabled,
- videoAvailable: manifest.videoAvailable,
- thresholds: options.gates,
- failed: failedGates,
- warnings: [...warningGates, ...diagnosticFailures.map(gate => ({ ...gate, status: 'warn', originalStatus: 'fail' }))]
- },
- tokenLeak: false,
- notes: ['Automated quality gates are directional; business review sample remains required before release decisions.']
- };
- }
- function evaluateRunGates({ run, baseline, manifest, options }) {
- if (!run) return [];
- const gates = [];
- const isResultFirst = /^result-first/.test(run.variant) || run.variant === 'video-enhanced';
- if (options.gates.requireLiveRecall && run.variant !== 'baseline-live') {
- gates.push(makeGate({
- name: 'live-recall',
- status: run.liveRecallCount > 0 ? 'pass' : 'fail',
- value: run.liveRecallCount,
- target: '> 0',
- note: 'Live variants must return real recalled candidates.'
- }));
- }
- if (manifest.videoAvailable && run.variant !== 'baseline-live') {
- gates.push(makeGate({
- name: 'evidence-coverage',
- status: run.top10EvidenceCoverage >= options.gates.minEvidenceCoverage ? 'pass' : 'fail',
- value: run.top10EvidenceCoverage,
- target: `>= ${options.gates.minEvidenceCoverage}`,
- note: 'Video/evidence-enabled runs should cover most top candidates.'
- }));
- }
- gates.push(makeGate({
- name: 'negative-risk-rate',
- status: negativeRiskRateOf(run) <= options.gates.maxNegativeRiskRate ? 'pass' : 'fail',
- value: negativeRiskRateOf(run),
- target: `<= ${options.gates.maxNegativeRiskRate}`,
- note: 'Top list must avoid negative samples such as off-brief, tone mismatch, weak homepage quality, or unlike reference accounts.'
- }));
- gates.push(makeGate({
- name: 'software-table-dedup',
- status: (run.softwareTable?.duplicateKeyCount || 0) === 0 ? 'pass' : 'fail',
- value: run.softwareTable?.duplicateKeyCount || 0,
- target: '= 0',
- note: 'Software handoff table must not contain duplicate creator keys.'
- }));
- gates.push(makeGate({
- name: 'software-rank-continuous',
- status: run.softwareTable?.rankContinuous ? 'pass' : 'fail',
- value: run.softwareTable?.rankSequence || '',
- target: '1..n',
- note: 'Software handoff table ranks must be continuous after dedupe and exclusion.'
- }));
- gates.push(makeGate({
- name: 'homepage-evidence-status',
- status: run.providerStatus?.homepage?.status ? 'pass' : 'fail',
- value: run.providerStatus?.homepage?.status || 'missing',
- target: 'present',
- note: 'Each run must expose homepage evidence provider or fallback status.'
- }));
- if (options.gates.requireReferenceProvider && run.variant === 'reference-account') {
- const referenceStatus = run.providerStatus?.reference?.status || run.providerStatus?.reference?.providerStatus || 'missing';
- gates.push(makeGate({
- name: 'reference-provider-ok',
- status: referenceStatus === 'ok' ? 'pass' : 'fail',
- value: referenceStatus,
- target: 'ok',
- note: 'reference-account strategy must prove real reference enrichment when strict provider gate is enabled.'
- }));
- }
- if (options.gates.requireHomepageProvider && run.variant === 'homepage-evidence') {
- const homepageStatus = run.providerStatus?.homepage?.status || 'missing';
- gates.push(makeGate({
- name: 'homepage-provider-ok',
- status: homepageStatus === 'ok' ? 'pass' : 'fail',
- value: homepageStatus,
- target: 'ok',
- note: 'homepage-evidence strategy must prove real recent homepage evidence when strict provider gate is enabled.'
- }));
- }
- if (baseline && isResultFirst) {
- if (options.gates.requireResultFirstNoStrongDrop) {
- gates.push(makeGate({
- name: 'strong-count-not-degraded',
- status: run.strongCount >= baseline.strongCount ? 'pass' : 'fail',
- value: run.strongCount,
- target: `>= baseline ${baseline.strongCount}`,
- note: 'Optimization variants must not lose strong candidates versus baseline.'
- }));
- }
- gates.push(makeGate({
- name: 'top10-score-not-degraded',
- status: run.avgTop10TotalScore >= baseline.avgTop10TotalScore - options.gates.maxScoreDrop ? 'pass' : 'fail',
- value: run.avgTop10TotalScore,
- target: `>= baseline ${round(baseline.avgTop10TotalScore - options.gates.maxScoreDrop)}`,
- note: 'Optimization variants must not buy evidence at the cost of lower list quality.'
- }));
- }
- return gates.map(gate => ({ ...gate, briefId: run.briefId, variant: run.variant, gateRole: run.gateRole || 'release' }));
- }
- function runGateScore(run) {
- if (!run) return -Infinity;
- const failCount = (run.gates || []).filter(gate => gate.status === 'fail').length;
- return run.strongCount * 100 +
- run.avgTop10TotalScore * 2 +
- run.top10EvidenceCoverage * 20 -
- failCount * 1000;
- }
- function makeGate({ name, status, value, target, note }) {
- return { name, status, value, target, note };
- }
- function renderAggregateReport(aggregate) {
- const lines = [
- '# 提号 overnight 质量验证报告',
- '',
- `- 开始时间:${aggregate.manifest.startedAt}`,
- `- 结束时间:${aggregate.finishedAt}`,
- `- 是否 live:${aggregate.manifest.liveEnabled}`,
- `- 运行次数:${aggregate.runCount}`,
- `- 程序失败数:${aggregate.failureCount}`,
- `- 总门禁是否通过:${aggregate.acceptance.overallPass}`,
- `- 发布策略覆盖是否通过:${aggregate.acceptance.releaseCoveragePass}`,
- `- 发布门禁失败数:${aggregate.acceptance.failedGateCount}`,
- `- 诊断 warning 数:${aggregate.acceptance.warningGateCount}`,
- `- result-first 胜率:${Math.round(aggregate.acceptance.resultFirstWinRate * 100)}%`,
- '',
- '## 验收门禁',
- '',
- `- 是否要求 live 召回:${aggregate.acceptance.gates.thresholds.requireLiveRecall}`,
- `- 最低证据覆盖:${pct(aggregate.acceptance.gates.thresholds.minEvidenceCoverage)}`,
- `- 最高负样本风险率:${pct(aggregate.acceptance.gates.thresholds.maxNegativeRiskRate)}`,
- `- top-10 均分最大允许下降:${aggregate.acceptance.gates.thresholds.maxScoreDrop}`,
- '',
- '| 状态 | 角色 | brief | 策略 | 门禁 | 当前值 | 目标 |',
- '| --- | --- | --- | --- | --- | ---: | --- |',
- ...renderGateRows(aggregate),
- '',
- '## 各 brief 最佳策略',
- '',
- '| Brief | 最佳策略 | 强推数 | 候选数 | 证据覆盖 | 平均分 | 平均参考风格分 | 负样本风险率 |',
- '| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |',
- ...aggregate.byBrief.map(item => {
- const best = item.best || {};
- return `| ${escapeCell(item.briefName)} | ${best.variant || 'n/a'} | ${best.strongCount || 0} | ${best.uniqueCandidateCount || 0} | ${pct(best.top10EvidenceCoverage)} | ${best.avgTop10TotalScore || 0} | ${best.avgTop10ReferenceStyleFitScore || 0} | ${pct(negativeRiskRateOf(best))} |`;
- }),
- '',
- '## 各策略指标',
- ''
- ];
- for (const brief of aggregate.byBrief) {
- lines.push(`### ${brief.briefName}`, '');
- lines.push('| 策略 | 角色 | 强推数 | 候选数 | 召回数 | 证据覆盖 | 平均分 | 平均参考分 | 负样本风险率 | 门禁失败数 |');
- lines.push('| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |');
- for (const run of aggregate.runs.filter(item => item.briefId === brief.briefId)) {
- const briefRun = brief.runs.find(item => item.variant === run.variant) || {};
- const failCount = (briefRun.gates || []).filter(gate => gate.status === 'fail').length;
- lines.push(`| ${run.variant} | ${run.gateRole || 'release'} | ${run.strongCount} | ${run.uniqueCandidateCount} | ${run.liveRecallCount} | ${pct(run.top10EvidenceCoverage)} | ${run.avgTop10TotalScore} | ${run.avgTop10ReferenceStyleFitScore} | ${pct(negativeRiskRateOf(run))} | ${failCount} |`);
- }
- lines.push('');
- }
- lines.push('## 程序失败', '');
- if (!aggregate.failures.length) lines.push('- 无');
- for (const failure of aggregate.failures) {
- lines.push(`- ${failure.fixtureId}/${failure.variant}: ${escapeCell(failure.message).slice(0, 240)}`);
- }
- lines.push('', '## 人工复核说明', '');
- lines.push('- 打开 `manual-review-sample.csv` 标注人工复核标签:可直接发客户 / 商务复核 / 可投但需补证 / 跑偏 / 硬性规则违规 / 调性不符 / 主页质感不符 / 参考账号不像。');
- lines.push('- 负样本必须填写“归因类型”,可选:需求解析错 / 隐性规则漏 / 召回关键词错 / 主页证据不足 / 视频证据误判 / 排序权重错 / 输出解释错 / 软件端表重复或排名不连续。');
- lines.push('- 有客户反馈时填写“客户选择”:客户选中 / 客户拒绝 / 待客户反馈。模型证据只用于辅助筛选,不能替代最终合规、报价和主页有效性确认。');
- return lines.join('\n');
- }
- function renderGateRows(aggregate) {
- const gates = [
- ...(aggregate.acceptance.gates.failed || []),
- ...(aggregate.acceptance.gates.warnings || [])
- ];
- if (!gates.length) return ['| n/a | n/a | n/a | n/a | n/a | 0 | n/a |'];
- return gates
- .slice(0, 80)
- .map(gate => `| ${gate.status} | ${escapeCell(gate.gateRole || 'release')} | ${escapeCell(gate.briefId)} | ${escapeCell(gate.variant)} | ${escapeCell(gate.name)} | ${escapeCell(gate.value)} | ${escapeCell(gate.target)} |`);
- }
- function renderAggregateReportClean(aggregate) {
- const lines = [
- '# 提号 overnight 质量验证报告',
- '',
- `- 开始时间:${aggregate.manifest.startedAt}`,
- `- 结束时间:${aggregate.finishedAt}`,
- `- 是否 live:${aggregate.manifest.liveEnabled}`,
- `- 运行次数:${aggregate.runCount}`,
- `- 程序失败数:${aggregate.failureCount}`,
- `- 总门禁是否通过:${aggregate.acceptance.overallPass}`,
- `- 发布策略覆盖是否通过:${aggregate.acceptance.releaseCoveragePass}`,
- `- 发布门禁失败数:${aggregate.acceptance.failedGateCount}`,
- `- warning 数:${aggregate.acceptance.warningGateCount}`,
- '',
- '## 新增硬验收',
- '',
- '- 软件端表重复键必须为 0。',
- '- 软件端表排名必须从 1 开始连续。',
- '- 每个 run 必须输出主页证据状态,状态可以是真实 provider,也可以是 fallback/not_requested。',
- '',
- '## 各 Brief 最佳策略',
- '',
- '| Brief | 最佳策略 | 强推荐数 | 候选数 | 软件重复键 | 排名连续 | 参考补证状态 | 主页证据状态 | 证据覆盖 | 平均分 | 负样本风险率 |',
- '| --- | --- | ---: | ---: | ---: | --- | --- | --- | ---: | ---: | ---: |'
- ];
- for (const item of aggregate.byBrief) {
- const best = item.best || {};
- lines.push(`| ${escapeCell(item.briefName)} | ${best.variant || 'n/a'} | ${best.strongCount || 0} | ${best.uniqueCandidateCount || 0} | ${best.softwareTable?.duplicateKeyCount ?? 0} | ${best.softwareTable?.rankContinuous ? '是' : '否'} | ${best.providerStatus?.reference?.status || best.providerStatus?.reference?.providerStatus || 'missing'} | ${best.providerStatus?.homepage?.status || 'missing'} | ${pct(best.top10EvidenceCoverage)} | ${best.avgTop10TotalScore || 0} | ${pct(negativeRiskRateOf(best))} |`);
- }
- lines.push('', '## 各策略指标', '');
- for (const brief of aggregate.byBrief) {
- lines.push(`### ${brief.briefName}`, '');
- lines.push('| 策略 | 角色 | 强推荐数 | 候选数 | 软件重复键 | 排名连续 | 参考补证状态 | 主页证据状态 | 召回数 | 证据覆盖 | 平均分 | 参考风格分 | 负样本风险率 | 失败门禁数 |');
- lines.push('| --- | --- | ---: | ---: | ---: | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |');
- for (const run of aggregate.runs.filter(item => item.briefId === brief.briefId)) {
- const briefRun = brief.runs.find(item => item.variant === run.variant) || {};
- const failCount = (briefRun.gates || []).filter(gate => gate.status === 'fail').length;
- lines.push(`| ${run.variant} | ${run.gateRole || 'release'} | ${run.strongCount} | ${run.uniqueCandidateCount} | ${run.softwareTable?.duplicateKeyCount ?? 0} | ${run.softwareTable?.rankContinuous ? '是' : '否'} | ${run.providerStatus?.reference?.status || run.providerStatus?.reference?.providerStatus || 'missing'} | ${run.providerStatus?.homepage?.status || 'missing'} | ${run.liveRecallCount} | ${pct(run.top10EvidenceCoverage)} | ${run.avgTop10TotalScore} | ${run.avgTop10ReferenceStyleFitScore} | ${pct(negativeRiskRateOf(run))} | ${failCount} |`);
- }
- lines.push('');
- }
- lines.push('## 门禁失败与警告', '');
- const gates = [...(aggregate.acceptance.gates.failed || []), ...(aggregate.acceptance.gates.warnings || [])];
- if (!gates.length) lines.push('- 无');
- for (const gate of gates.slice(0, 80)) {
- lines.push(`- [${gate.status}] ${gate.briefId}/${gate.variant}/${gate.name}: ${gate.value},目标 ${gate.target}`);
- }
- lines.push('', '## 程序失败', '');
- if (!aggregate.failures.length) lines.push('- 无');
- for (const failure of aggregate.failures) {
- lines.push(`- ${failure.fixtureId}/${failure.variant}: ${escapeCell(failure.message).slice(0, 240)}`);
- }
- lines.push('', '## 人工复核说明', '');
- lines.push('- 打开 `manual-review-sample.csv` 标注人工复核标签:可直接发客户 / 商务复核 / 可投但需补证 / 跑偏 / 硬性规则违规 / 调性不符 / 主页质感不符 / 参考账号不像。');
- lines.push('- 负样本必须填写“归因类型”,可选:需求解析错 / 隐性规则漏 / 召回关键词错 / 主页证据不足 / 视频证据误判 / 排序权重错 / 输出解释错 / 软件端表重复或排名不连续。');
- lines.push('- 有客户反馈时填写“客户选择”:客户选中 / 客户拒绝 / 待客户反馈。自动化门禁只证明结构和方向,真实客户选中率仍需要客户最终选择数据验证。');
- return lines.join('\n');
- }
- function buildManualReviewSample(runs) {
- const header = [
- 'brief编号',
- '策略',
- '排名',
- '平台',
- '博主名称',
- '综合分',
- 'brief匹配分',
- '参考风格分',
- '主页证据分',
- '视觉质感分',
- '调性一致分',
- '证据加分',
- '证据风险扣分',
- '推荐理由',
- '风险提示',
- '主页链接',
- '人工复核标签',
- '客户选择',
- '归因类型',
- '反馈原因'
- ];
- const best = new Map();
- for (const run of runs.filter(item => item.variant !== 'baseline-live')) {
- for (const item of (run.top10 || []).slice(0, 10)) {
- const platform = item.platform || '';
- const profileUrl = item.profileUrl || '';
- const displayName = item.displayName || '';
- const key = profileUrl
- ? `${run.briefId}|${platform}|url:${profileUrl.toLowerCase()}`
- : `${run.briefId}|${platform}|name:${displayName.toLowerCase()}`;
- const row = {
- briefId: run.briefId,
- variant: run.variant,
- platform,
- displayName,
- score: Number(item.score || 0),
- briefFitScore: Number(item.briefFitScore || 0),
- referenceStyleFitScore: Number(item.referenceStyleFitScore || 0),
- recentContentFitScore: Number(item.recentContentFitScore || 0),
- visualQualityScore: Number(item.visualQualityScore || 0),
- toneConsistencyScore: Number(item.toneConsistencyScore || 0),
- evidenceScoreBoost: Number(item.evidenceScoreBoost || 0),
- evidenceRiskPenalty: Number(item.evidenceRiskPenalty || 0),
- reason: item.reason || '',
- riskNote: item.riskNote || '',
- profileUrl
- };
- const previous = best.get(key);
- if (!previous || reviewRowPriority(row) > reviewRowPriority(previous)) best.set(key, row);
- }
- }
- const rows = rankReviewRowsWithinBrief([...best.values()]
- .sort((a, b) =>
- String(a.briefId).localeCompare(String(b.briefId)) ||
- b.score - a.score ||
- b.briefFitScore - a.briefFitScore ||
- String(a.displayName).localeCompare(String(b.displayName)))
- .slice(0, 60))
- .map((item) => [
- item.briefId,
- item.variant,
- item.rank,
- item.platform,
- item.displayName,
- item.score,
- item.briefFitScore,
- item.referenceStyleFitScore,
- item.recentContentFitScore,
- item.visualQualityScore,
- item.toneConsistencyScore,
- item.evidenceScoreBoost,
- item.evidenceRiskPenalty,
- item.reason,
- item.riskNote,
- item.profileUrl,
- '',
- '',
- '',
- ''
- ]);
- return [header.join(','), ...rows.map(row => row.map(csvCell).join(','))].join('\n');
- }
- function rankReviewRowsWithinBrief(rows) {
- const counters = new Map();
- return rows.map(row => {
- const briefId = String(row.briefId || '未命名brief');
- const next = (counters.get(briefId) || 0) + 1;
- counters.set(briefId, next);
- return { ...row, rank: next };
- });
- }
- function reviewRowPriority(row) {
- return Number(row.score || 0) * 10000 +
- Number(row.briefFitScore || 0) * 100 +
- Number(row.referenceStyleFitScore || 0);
- }
- function loadFixtures(dir) {
- return fs.readdirSync(dir)
- .filter(name => name.endsWith('.json'))
- .sort()
- .map(name => JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')));
- }
- function csvSet(value) {
- return new Set(String(value || '').split(',').map(item => item.trim()).filter(Boolean));
- }
- function intEnv(name, defaultValue) {
- const value = Number.parseInt(process.env[name] || '', 10);
- return Number.isFinite(value) && value >= 0 ? value : defaultValue;
- }
- function numberEnv(name, defaultValue) {
- const value = Number.parseFloat(process.env[name] || '');
- return Number.isFinite(value) ? value : defaultValue;
- }
- function qualityScore(run, baseline) {
- if (!run) return -Infinity;
- const baseScore = baseline ? baseline.avgTop10TotalScore : 0;
- return run.strongCount * 8 +
- run.avgTop10TotalScore * 2 +
- run.avgTop10ReferenceStyleFitScore +
- run.top10EvidenceCoverage * 20 -
- negativeRiskRateOf(run) * 40 +
- Math.max(0, run.avgTop10TotalScore - baseScore) * 3;
- }
- function negativeRiskRateOf(run) {
- if (!run) return 0;
- return run.negativeRiskRate ?? run.offTopicRate ?? 0;
- }
- function hasOffTopicHit(item, terms) {
- return findOffTopicHits(item, terms).length > 0;
- }
- function findOffTopicHits(item, terms) {
- const text = [
- item.displayName,
- item.recommendReason,
- item.riskNote,
- ...(item.contentTags || []),
- ...(item.personaTags || []),
- ...(item.evidenceSignals || []),
- ...(item.evidenceRiskHints || [])
- ].join(' ');
- return (terms || []).filter(term => term && text.includes(term));
- }
- function countStatus(candidates, status) {
- return candidates.filter(item => item.recommendStatus === status).length;
- }
- function avg(values) {
- const nums = values.map(Number).filter(Number.isFinite);
- return nums.length ? round(nums.reduce((sum, value) => sum + value, 0) / nums.length) : 0;
- }
- function ratio(numerator, denominator) {
- return denominator ? round(Number(numerator || 0) / Number(denominator)) : 0;
- }
- function round(value) {
- return Math.round(Number(value || 0) * 100) / 100;
- }
- function pct(value) {
- return `${Math.round(Number(value || 0) * 100)}%`;
- }
- function writeJson(file, value) {
- fs.mkdirSync(path.dirname(file), { recursive: true });
- fs.writeFileSync(file, JSON.stringify(value, null, 2));
- }
- function writeText(file, value) {
- fs.mkdirSync(path.dirname(file), { recursive: true });
- fs.writeFileSync(file, withExcelBom(file, value), 'utf8');
- }
- function withExcelBom(file, value) {
- const text = String(value ?? '');
- if (!/\.(csv|md)$/i.test(String(file || ''))) return text;
- return text.startsWith('\uFEFF') ? text : `\uFEFF${text}`;
- }
- function listRunFiles(dir) {
- if (!fs.existsSync(dir)) return [];
- return fs.readdirSync(dir).map(name => path.join(dir, name));
- }
- function inspectSoftwareTable(csvPath) {
- const expectedHeader = 'brief编号,策略,排名,平台,博主名称,综合分,brief匹配分,参考风格分,主页证据分,视觉质感分,调性一致分,证据加分,证据风险扣分,推荐理由,风险提示,主页链接,人工复核标签';
- if (!fs.existsSync(csvPath)) {
- return {
- exists: false,
- headerOk: false,
- rowCount: 0,
- duplicateKeyCount: 0,
- rankContinuous: false,
- rankSequence: ''
- };
- }
- const text = fs.readFileSync(csvPath, 'utf8').replace(/^\uFEFF/, '');
- const lines = text.trim().split(/\r?\n/).filter(Boolean);
- const rows = lines.slice(1).map(parseCsvLine);
- const seen = new Set();
- let duplicateKeyCount = 0;
- for (const row of rows) {
- const key = row[15] ? `${row[0]}|${row[3]}|${row[15]}` : `${row[0]}|${row[3]}|${row[4]}`;
- if (seen.has(key)) duplicateKeyCount += 1;
- seen.add(key);
- }
- const ranks = rows.map(row => Number(row[2]));
- const rankContinuous = ranksContinuousWithinBrief(rows);
- return {
- exists: true,
- headerOk: lines[0] === expectedHeader,
- rowCount: rows.length,
- duplicateKeyCount,
- rankContinuous,
- rankSequence: ranks.join(',')
- };
- }
- function ranksContinuousWithinBrief(rows) {
- const counters = new Map();
- for (const row of rows) {
- const briefId = row[0] || '未命名brief';
- const expected = (counters.get(briefId) || 0) + 1;
- if (Number(row[2]) !== expected) return false;
- counters.set(briefId, expected);
- }
- return true;
- }
- function parseCsvLine(line) {
- const cells = [];
- let current = '';
- let quoted = false;
- for (let i = 0; i < line.length; i += 1) {
- const char = line[i];
- if (char === '"' && quoted && line[i + 1] === '"') {
- current += '"';
- i += 1;
- } else if (char === '"') {
- quoted = !quoted;
- } else if (char === ',' && !quoted) {
- cells.push(current);
- current = '';
- } else {
- current += char;
- }
- }
- cells.push(current);
- return cells;
- }
- function scanForLeaks(root, secrets) {
- const realSecrets = [...new Set((secrets || []).filter(Boolean))];
- for (const file of listFiles(root)) {
- const text = fs.readFileSync(file, 'utf8');
- for (const secret of realSecrets) {
- if (text.includes(secret)) throw new Error(`Secret leaked into ${file}`);
- }
- if (/Authorization\s*:/i.test(text)) throw new Error(`Authorization header leaked into ${file}`);
- }
- }
- function listFiles(root) {
- return fs.readdirSync(root, { withFileTypes: true }).flatMap(entry => {
- const full = path.join(root, entry.name);
- return entry.isDirectory() ? listFiles(full) : [full];
- });
- }
- function csvCell(value) {
- const text = String(value ?? '');
- return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
- }
- function escapeCell(value) {
- return String(value || '').replace(/\|/g, '/').replace(/\n/g, ' ');
- }
- function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- main().catch(error => {
- console.error(redactSecrets(error && error.stack ? error.stack : String(error)));
- process.exit(1);
- });
|