optbrew 0.1.1 → 0.1.2
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/CHANGELOG.md +30 -0
- package/README.ko.md +78 -2
- package/README.md +78 -2
- package/docs/platforms.md +80 -4
- package/docs/release.md +2 -2
- package/lib/cli.mjs +15 -2
- package/lib/doctor.mjs +1 -0
- package/lib/reinstall.mjs +92 -0
- package/lib/update.mjs +10 -2
- package/package.json +2 -2
- package/plugin.json +2 -2
- package/skills/optbrew/SKILL.md +2 -2
- package/skills/optbrew/bundle-manifest.json +39 -15
- package/skills/optbrew/modules/naver-searchadvisor/scripts/audit.mjs +29 -1
- package/skills/optbrew/references/ai-crawlers.json +21 -0
- package/skills/optbrew/references/ai-crawlers.md +24 -0
- package/skills/optbrew/references/api.md +1 -1
- package/skills/optbrew/references/report.md +4 -0
- package/skills/optbrew/references/scoring.md +1 -1
- package/skills/optbrew/scripts/audit.mjs +8 -29
- package/skills/optbrew/scripts/core/bundle.mjs +1 -1
- package/skills/optbrew/scripts/core/collector.mjs +30 -16
- package/skills/optbrew/scripts/core/crawlers.mjs +11 -0
- package/skills/optbrew/scripts/core/derive.mjs +168 -0
- package/skills/optbrew/scripts/core/facts.mjs +189 -0
- package/skills/optbrew/scripts/core/html.mjs +53 -0
- package/skills/optbrew/scripts/core/report.mjs +132 -42
- package/skills/optbrew/source-lock.json +1 -1
- package/source-lock.json +1 -1
|
@@ -10,7 +10,9 @@ import { measurePanel } from './metrics.mjs';
|
|
|
10
10
|
import { normalizeImport } from '../adapters/imports.mjs';
|
|
11
11
|
import { mergeFindings, selectRemediations } from './findings.mjs';
|
|
12
12
|
import { auditCollected, indexingDirectives } from '../../modules/naver-searchadvisor/scripts/audit.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import { deriveObservations } from './derive.mjs';
|
|
14
|
+
import { links } from './collector.mjs';
|
|
15
|
+
import { assertUrlInScope } from './scope.mjs';
|
|
14
16
|
import { parseRobots, robotsVerdict } from '../../modules/naver-searchadvisor/scripts/robots.mjs';
|
|
15
17
|
|
|
16
18
|
const catalog = JSON.parse(readFileSync(new URL('../../references/remediations.json', import.meta.url))).items;
|
|
@@ -34,6 +36,28 @@ function completeWindow(metric) {
|
|
|
34
36
|
return completeDate <= dayBefore(localCollectionDate) && metric.window.end <= completeDate;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
/** The optbrew operating rule (design spec, scoring.md): at least 80% of the applicable weight must be measured. */
|
|
40
|
+
export const COVERAGE_THRESHOLD = 80;
|
|
41
|
+
|
|
42
|
+
/** One headline per profile. Below the coverage threshold the single number is withheld and only the measured subset is shown. */
|
|
43
|
+
export function scoreHeadline(result) {
|
|
44
|
+
const exact = result.A > 0 ? 100 * result.M / result.A : null;
|
|
45
|
+
const coverageOk = result.score !== null && exact !== null && exact >= COVERAGE_THRESHOLD - 1e-9;
|
|
46
|
+
// The design spec also asks for verdicts on the core access and indexing checks before a score counts as sufficient.
|
|
47
|
+
const coreOk = (result.unknown_core ?? []).length === 0;
|
|
48
|
+
const status = !coverageOk ? 'insufficient_coverage' : !coreOk ? 'insufficient_core' : 'sufficient';
|
|
49
|
+
return { profile: result.profile, status, headline_score: status === 'sufficient' ? result.score : null,
|
|
50
|
+
measured_subset_score: result.score, coverage: result.coverage, coverage_exact: exact, coverage_threshold: COVERAGE_THRESHOLD,
|
|
51
|
+
unresolved_core: (result.unknown_core ?? []).length, missing_bounds: result.missing_bounds, readiness_status: result.readiness_status };
|
|
52
|
+
}
|
|
53
|
+
const floorTenth = value => value === null ? 'unknown' : Math.floor(value * 10 + 1e-9) / 10;
|
|
54
|
+
const headlineCell = result => {
|
|
55
|
+
const headline = scoreHeadline(result);
|
|
56
|
+
return headline.status === 'sufficient' ? `${headline.headline_score} (coverage ${headline.coverage}%)` :
|
|
57
|
+
headline.status === 'insufficient_core' ? `insufficient core checks (${headline.unresolved_core} unresolved; measured subset ${headline.measured_subset_score ?? 'unknown'}; coverage ${headline.coverage ?? 'unknown'}%; bounds ${headline.missing_bounds?.join('–') ?? 'unknown'})` :
|
|
58
|
+
`insufficient coverage (measured subset ${headline.measured_subset_score ?? 'unknown'}; coverage ${headline.coverage ?? 'unknown'}%; bounds ${headline.missing_bounds?.join('–') ?? 'unknown'})`;
|
|
59
|
+
};
|
|
60
|
+
|
|
37
61
|
function metricRows(metrics) {
|
|
38
62
|
const groups = new Map();
|
|
39
63
|
for (const metric of metrics) {
|
|
@@ -327,6 +351,74 @@ function statusFor(finding, changes, afterInput, target, requireRef) {
|
|
|
327
351
|
return linked.every(change => change.environment === 'local') && localResolved ? 'verified_local' : 'modified_unverified';
|
|
328
352
|
}
|
|
329
353
|
|
|
354
|
+
/**
|
|
355
|
+
* The request ledger accounts for every queued or rejected URL. Collection counts and the skipped list must equal what
|
|
356
|
+
* the ledger and the hashed response records show, so a fetched page cannot be moved to skipped and a request limit
|
|
357
|
+
* cannot be claimed without the requests that exhaust it.
|
|
358
|
+
*/
|
|
359
|
+
function checkLedger(input, audit, collection, artifacts, requireRef) {
|
|
360
|
+
const ledger = JSON.parse(requireRef(audit.ledger_ref).text);
|
|
361
|
+
const entries = ledger.entries;
|
|
362
|
+
const fail = message => { throw new ContractError(`request ledger: ${message}`); };
|
|
363
|
+
if (!Array.isArray(entries)) fail('entries missing');
|
|
364
|
+
const limit = Math.min(input.run_context.scope?.max_urls ?? 50, 50);
|
|
365
|
+
if (ledger.limit !== limit) fail('request limit differs from the run scope');
|
|
366
|
+
const total = entries.reduce((sum, entry) => sum + entry.requests, 0);
|
|
367
|
+
// The limit decides whether a fetch may start; a redirect chain that has started finishes. A chain is at most
|
|
368
|
+
// min(10, limit) redirects long and at most `concurrency` chains are in flight, so the overshoot is bounded by both.
|
|
369
|
+
const concurrency = Math.min(input.run_context.scope?.concurrency ?? 2, 2);
|
|
370
|
+
if (total > limit + concurrency * Math.min(10, limit)) fail('more requests than the limit');
|
|
371
|
+
const responded = artifacts.filter(x => Number.isInteger(x.status));
|
|
372
|
+
const failed = artifacts.filter(x => !Number.isInteger(x.status));
|
|
373
|
+
const byUrl = outcome => new Map(entries.filter(x => x.outcome === outcome).map(x => [x.url, x]));
|
|
374
|
+
const responses = byUrl('response'), errors = byUrl('error');
|
|
375
|
+
if (responses.size !== responded.length || audit.responses.length !== responded.length || errors.size !== failed.length)
|
|
376
|
+
fail('collected artifacts differ from the ledger or the response records');
|
|
377
|
+
for (const artifact of responded) {
|
|
378
|
+
const entry = responses.get(artifact.normalized_url);
|
|
379
|
+
if (!entry || entry.requests !== artifact.redirect_chain.length + 1) fail(`response for ${artifact.normalized_url} is not recorded`);
|
|
380
|
+
}
|
|
381
|
+
// A skipped entry never reached the network. Requests booked on it would only pad the total toward a claimed limit.
|
|
382
|
+
if (entries.some(x => x.outcome === 'skipped' && x.requests !== 0)) fail('a skipped entry carries requests');
|
|
383
|
+
for (const artifact of failed) {
|
|
384
|
+
const entry = errors.get(artifact.normalized_url);
|
|
385
|
+
if (!entry) fail(`failed fetch of ${artifact.normalized_url} is not recorded`);
|
|
386
|
+
// A failed fetch has no hashed response record, so nothing proves how many requests it made: it is always one.
|
|
387
|
+
// Neither the redirect chain nor any count inside collection.json can raise it, so a forger cannot pad the total.
|
|
388
|
+
if (entry.requests !== 1) fail(`failed fetch of ${artifact.normalized_url} is not booked as exactly one request`);
|
|
389
|
+
}
|
|
390
|
+
const key = x => `${x.url}|${x.reason ?? x.skipped_reason}`;
|
|
391
|
+
const ledgerSkips = entries.filter(x => x.skipped_reason).map(key).sort();
|
|
392
|
+
if (JSON.stringify((collection.skipped ?? []).map(key).sort()) !== JSON.stringify(ledgerSkips)) fail('skipped list differs from the ledger');
|
|
393
|
+
const limited = entries.some(x => ['url_limit', 'request_limit'].includes(x.skipped_reason));
|
|
394
|
+
if (collection.limit_reached !== limited) fail('limit_reached differs from the ledger');
|
|
395
|
+
if (limited && total < limit) fail('a request limit is claimed but fewer requests than the limit were made');
|
|
396
|
+
if (collection.request_count !== total || collection.evaluated_count !== entries.filter(x => x.requests > 0).length) fail('request or evaluated count differs from the ledger');
|
|
397
|
+
const selected = new Set(input.run_context.protocol.page_types.flatMap(type => type.urls.map(x => x.normalized)));
|
|
398
|
+
if (entries.some(x => selected.has(x.url) && x.skipped_reason && x.skipped_reason !== 'url_limit' && x.outcome === 'skipped')) fail('a selected page is skipped for a reason other than the request limit');
|
|
399
|
+
// The protocol caps selected pages plus auxiliary resources at the limit, and the collector queues them before any
|
|
400
|
+
// discovered page. A selected page can only hit the request limit after redirects used the budget, so no page that was
|
|
401
|
+
// merely discovered may have been fetched in the same run.
|
|
402
|
+
const auxiliary = new Set((input.run_context.protocol.auxiliary_resources ?? []).map(x => x.normalized));
|
|
403
|
+
const planned = new Set([...selected, ...auxiliary]);
|
|
404
|
+
// Provenance: a failed fetch or a request-limit skip belongs to a URL the run was given (selected or auxiliary) or one
|
|
405
|
+
// that a hashed page of this collection links to (anchors and canonical, as the collector follows them). A URL nothing
|
|
406
|
+
// links to cannot be a failed fetch or a skipped one, so fabricated entries cannot pad the total or claim a limit.
|
|
407
|
+
const known = new Set(planned);
|
|
408
|
+
for (const artifact of responded) {
|
|
409
|
+
if (artifact.status !== 200 || artifact.capability !== 'raw_html' || auxiliary.has(artifact.normalized_url)) continue;
|
|
410
|
+
for (const link of links(requireRef(artifact.evidence_ref).text, artifact.final_url)) {
|
|
411
|
+
try { known.add(assertUrlInScope(link, input.run_context.scope).href); } catch { /* out of scope: the collector skipped it */ }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
if ((entry.outcome === 'error' || (entry.outcome === 'skipped' && entry.skipped_reason === 'url_limit')) && !known.has(entry.url))
|
|
416
|
+
fail(`${entry.url} has no provenance: it is neither a selected or auxiliary URL nor linked from a collected page`);
|
|
417
|
+
}
|
|
418
|
+
if (entries.some(x => selected.has(x.url) && x.outcome === 'skipped' && x.skipped_reason === 'url_limit') &&
|
|
419
|
+
entries.some(x => x.requests > 0 && !planned.has(x.url))) fail('a selected page is skipped for the request limit although a discovered page was fetched');
|
|
420
|
+
}
|
|
421
|
+
|
|
330
422
|
function checkAuditPhase(input, phase, snapshot, requireRef) {
|
|
331
423
|
if (!input) return;
|
|
332
424
|
const observations = input.observations.filter(x => x.source_ref === 'optbrew:collector');
|
|
@@ -341,6 +433,10 @@ function checkAuditPhase(input, phase, snapshot, requireRef) {
|
|
|
341
433
|
const savedObservations = JSON.parse(requireRef(audit.observations_ref).text);
|
|
342
434
|
if (!Array.isArray(collection.artifacts) || !Array.isArray(audit.raw) || !Array.isArray(savedObservations))
|
|
343
435
|
throw new ContractError('audit snapshot files incomplete');
|
|
436
|
+
if (!Array.isArray(audit.modules) || !audit.ledger_ref || !Array.isArray(audit.responses))
|
|
437
|
+
throw new ContractError('audit evidence was produced by an older optbrew release (it has no request ledger or module list); re-run the audit with optbrew 0.1.2 and build a new packet');
|
|
438
|
+
for (const name of audit.modules) if (!Array.isArray(moduleOutputs[name])) throw new ContractError(`planned module output missing: ${name}`);
|
|
439
|
+
const naverPlanned = audit.modules.includes('naver-searchadvisor');
|
|
344
440
|
const rawRefs = new Set();
|
|
345
441
|
for (const raw of audit.raw) {
|
|
346
442
|
if (raw.content_hash === null) {
|
|
@@ -355,6 +451,7 @@ function checkAuditPhase(input, phase, snapshot, requireRef) {
|
|
|
355
451
|
}
|
|
356
452
|
// Final URL, redirect chain and header lines come from the hashed response records, not from collection.json alone.
|
|
357
453
|
const artifacts = collection.artifacts.map(x => boundArtifact(x, audit, requireRef));
|
|
454
|
+
checkLedger(input, audit, collection, artifacts, requireRef);
|
|
358
455
|
for (const obs of observations) {
|
|
359
456
|
if (!savedObservations.some(x => same(x, obs)) || !rawRefs.has(obs.evidence_ref))
|
|
360
457
|
throw new ContractError('collector observation is not bound to audit index and raw evidence');
|
|
@@ -374,21 +471,20 @@ function checkAuditPhase(input, phase, snapshot, requireRef) {
|
|
|
374
471
|
if (['index-policy', 'document-title'].includes(obs.check_id) &&
|
|
375
472
|
(['pass', 'fail'].includes(obs.state) || obs.check_id === 'document-title' && obs.state === 'unknown')) {
|
|
376
473
|
const raw = artifact && requireRef(obs.evidence_ref);
|
|
377
|
-
if (!module || !artifact || artifact.status !== 200 || artifact.capability !== 'raw_html' ||
|
|
474
|
+
if ((naverPlanned && !module) || !artifact || artifact.status !== 200 || artifact.capability !== 'raw_html' ||
|
|
378
475
|
artifact.raw_html_ref !== obs.evidence_ref || artifact.content_hash !== raw.sha256 || raw.kind !== 'raw_html')
|
|
379
476
|
throw new ContractError('title/index verdict lacks bound raw response');
|
|
380
477
|
const replay = auditCollected({ artifact, html: raw.text, engines });
|
|
381
|
-
if (!same([module.title, module.robots_meta, module.x_robots_tag],
|
|
478
|
+
if (naverPlanned && (!same([module.title, module.robots_meta, module.x_robots_tag],
|
|
382
479
|
[replay.title, replay.robots_meta, replay.x_robots_tag]) ||
|
|
383
480
|
(module.requested_url !== undefined && module.requested_url !== artifact.normalized_url) ||
|
|
384
|
-
(module.redirect_chain !== undefined && !same(module.redirect_chain, artifact.redirect_chain ?? [])))
|
|
481
|
+
(module.redirect_chain !== undefined && !same(module.redirect_chain, artifact.redirect_chain ?? []))))
|
|
385
482
|
throw new ContractError('title/index module differs from raw response and headers');
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
if (obs.state !== expected)
|
|
483
|
+
// Title states (missing, duplicated, unknown) are replayed by the derivation below.
|
|
484
|
+
if (obs.check_id === 'index-policy' && obs.state !== (replay.index_blocked ? 'fail' : 'pass'))
|
|
389
485
|
throw new ContractError('title/index verdict differs from raw response and headers');
|
|
390
486
|
}
|
|
391
|
-
if (obs.check_id === 'search-crawl-policy' && ['pass', 'fail'].includes(obs.state)) {
|
|
487
|
+
if (obs.check_id === 'search-crawl-policy' && ['pass', 'fail'].includes(obs.state) && obs.unit_key.split('|')[1] === 'naver') {
|
|
392
488
|
const [, engine] = obs.unit_key.split('|');
|
|
393
489
|
const robotsArtifact = artifacts.find(x => x.evidence_ref === obs.evidence_ref && x.status === 200);
|
|
394
490
|
const declared = robotsArtifact && (input.run_context.protocol.auxiliary_resources ?? []).some(resource =>
|
|
@@ -409,40 +505,26 @@ function checkAuditPhase(input, phase, snapshot, requireRef) {
|
|
|
409
505
|
throw new ContractError('crawl verdict differs from bound robots evidence and module output');
|
|
410
506
|
}
|
|
411
507
|
}
|
|
412
|
-
// Every
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
throw new ContractError(`selected page missing from the collection and its skipped list: ${url}`);
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
if (planned.has('fetch-response')) expect('fetch-response', null, url);
|
|
433
|
-
if (artifact.status !== 200 || artifact.capability !== 'raw_html') continue;
|
|
434
|
-
if (planned.has('document-title')) expect('document-title', null, url);
|
|
435
|
-
if (publicIndexing === false) continue;
|
|
436
|
-
if (planned.has('index-policy')) expect('index-policy', null, url);
|
|
437
|
-
if (planned.has('search-crawl-policy') && input.run_context.protocol.engines.includes('naver')) {
|
|
438
|
-
const robots = artifacts[declaredRobotsIndex(artifacts, declaredRobots, artifact.final_url)];
|
|
439
|
-
if (robots?.status === 200) {
|
|
440
|
-
const target = new URL(artifact.final_url);
|
|
441
|
-
if (!robotsVerdict(parseRobots(requireRef(robots.evidence_ref).text), 'yeti', target.pathname + target.search).allowed)
|
|
442
|
-
expect('search-crawl-policy', 'naver', url);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
508
|
+
// Every selected page is either collected or recorded as skipped; vanishing from both hides its observations.
|
|
509
|
+
for (const { normalized: url } of input.run_context.protocol.page_types.flatMap(type => type.urls))
|
|
510
|
+
if (!artifacts.some(x => x.normalized_url === url) && !(collection.skipped ?? []).some(x => x.url === url))
|
|
511
|
+
throw new ContractError(`selected page missing from the collection and its skipped list: ${url}`);
|
|
512
|
+
// Re-derive every collector verdict from the hashed raw evidence. Each derived observation must be present in the
|
|
513
|
+
// packet and in the saved observation file, and no collector observation may lack a derivation.
|
|
514
|
+
const derived = deriveObservations({ context: input.run_context, artifacts, collection,
|
|
515
|
+
textOf: artifact => artifact.content_hash ? requireRef(artifact.evidence_ref).text : null });
|
|
516
|
+
const shape = x => [x.state, x.q, x.partial_basis, x.evidence_ref, x.reason, x.evaluator_type, x.source_ref];
|
|
517
|
+
for (const expected of derived) {
|
|
518
|
+
if (!observations.some(x => x.unit_key === expected.unit_key) || !savedObservations.some(x => x.unit_key === expected.unit_key))
|
|
519
|
+
throw new ContractError(`planned collector observation missing: ${expected.unit_key}`);
|
|
520
|
+
const have = observations.find(x => x.unit_key === expected.unit_key);
|
|
521
|
+
if (!same(shape(have), shape(expected)))
|
|
522
|
+
throw new ContractError(expected.family_id === 'search-crawl-policy' ? 'crawl verdict differs from bound robots evidence and module output' :
|
|
523
|
+
['index-policy', 'document-title'].includes(expected.family_id) ? 'title/index verdict differs from raw response and headers' :
|
|
524
|
+
`collector verdict differs from raw evidence: ${expected.unit_key}`);
|
|
445
525
|
}
|
|
526
|
+
for (const have of observations) if (!derived.some(x => x.unit_key === have.unit_key))
|
|
527
|
+
throw new ContractError(`collector observation is not derivable from raw evidence: ${have.unit_key}`);
|
|
446
528
|
}
|
|
447
529
|
|
|
448
530
|
/** Recompute scores and metrics from contained run evidence; render four design tables. */
|
|
@@ -599,11 +681,18 @@ export function buildReport(packet) {
|
|
|
599
681
|
const scoreRows = baselineScores.map(before => {
|
|
600
682
|
const after = byProfile.get(before.profile);
|
|
601
683
|
const delta = comparison.score_deltas.find(x => x.profile === before.profile);
|
|
602
|
-
|
|
684
|
+
const cautious = scoreHeadline(before).status !== 'sufficient' || (after && scoreHeadline(after).status !== 'sufficient');
|
|
685
|
+
return [before.profile, headlineCell(before), after ? headlineCell(after) : 'unknown',
|
|
686
|
+
`${delta?.percentage_point_delta ?? 'unknown'}${cautious && delta ? ' (between measured subsets)' : ''}`,
|
|
603
687
|
`${before.coverage ?? 'unknown'} → ${after?.coverage ?? 'unknown'}`,
|
|
604
688
|
`${before.missing_bounds?.join('–') ?? 'unknown'} → ${after?.missing_bounds?.join('–') ?? 'unknown'}`,
|
|
605
689
|
`${before.readiness_status} → ${after?.readiness_status ?? 'unknown'}`];
|
|
606
690
|
});
|
|
691
|
+
const headlines = baselineScores.map(scoreHeadline);
|
|
692
|
+
result.headline = headlines;
|
|
693
|
+
const headlineText = headlines.map(h => h.status === 'sufficient' ? `${h.profile}: ${h.headline_score} (coverage ${h.coverage}%)` :
|
|
694
|
+
h.status === 'insufficient_core' ? `${h.profile}: insufficient core checks (${h.unresolved_core} access or indexing check(s) have no verdict, although ${h.coverage}% is measured); measured subset ${h.measured_subset_score ?? 'unknown'}, bounds ${h.missing_bounds?.join('–') ?? 'unknown'}` :
|
|
695
|
+
`${h.profile}: insufficient coverage (${floorTenth(h.coverage_exact)}% of the applicable weight is measured; at least ${h.coverage_threshold}% is needed); measured subset ${h.measured_subset_score ?? 'unknown'}, bounds ${h.missing_bounds?.join('–') ?? 'unknown'}`);
|
|
607
696
|
const remediationRows = findings.map(f => [f.priority, `${f.message} ${f.unit_key}`, f.sources.map(x => `${x.source_ref} ${x.evidence_ref}`).join('; '),
|
|
608
697
|
f.remediation ? `${f.remediation.mechanism} (${f.remediation.source_url}, checked ${f.remediation.checked_date})` : 'unknown',
|
|
609
698
|
packet.changes.filter(x => x.finding_ids.some(id => f.finding_ids.includes(id))).map(x => x.path).join('; ') || f.remediation?.location_hint || 'owner implementation location required',
|
|
@@ -614,6 +703,7 @@ export function buildReport(packet) {
|
|
|
614
703
|
const markdown = `# optbrew report\n\n${packet.data_class === 'synthetic' ? '**Synthetic example — no real site or provider outcome.**\n\n' : ''}` +
|
|
615
704
|
`Target: ${html(packet.run_context.target)} \nEnvironment: ${html(packet.run_context.environment)} \nGoals: ${html(packet.run_context.goals.join(', '))} \n` +
|
|
616
705
|
`Comparison: ${html(comparison.status)} (${html(comparison.reasons.join(', ') || 'none')}) \n` +
|
|
706
|
+
`Headline: \n${headlineText.map(line => `- ${html(line)} \n`).join('')}` +
|
|
617
707
|
`Baseline blockers: ${html(baselineScores[0]?.blockers.length ?? 0)} (${html(baselineScores[0]?.blockers.map(x => `${x.family_id} ${x.unit_key}`).join('; ') || 'none')}) → ` +
|
|
618
708
|
`After blockers: ${afterScores ? html(afterScores[0]?.blockers.length ?? 0) : 'unknown'} (${afterScores ? html(afterScores[0]?.blockers.map(x => `${x.family_id} ${x.unit_key}`).join('; ') || 'none') : 'after not observed'}); ` +
|
|
619
709
|
`after core unknown: ${afterScores ? html(afterScores[0]?.unknown_core.length ?? 0) : 'unknown'}; actual outcomes: ${packet.metrics.length ? 'see measured and missing states below' : 'not_connected / unknown'} \n` +
|
|
@@ -395,7 +395,7 @@
|
|
|
395
395
|
"package_path": "skills/optbrew/modules/naver-searchadvisor/scripts/audit.mjs",
|
|
396
396
|
"original_git_blob_sha": "de8c4a3430633cb2445e339d7a943d944cbf45a9",
|
|
397
397
|
"original_sha256": "3fd83141a00a9d45d4fbffa9d8ff515c7a49e7b1ea4637844e92375ea7797ff9",
|
|
398
|
-
"distributed_sha256": "
|
|
398
|
+
"distributed_sha256": "fa70051e07b429ad9477b8d778232cc4b3ca0bae8d2a1352074dfd4cb6f5f884",
|
|
399
399
|
"transformations": [
|
|
400
400
|
"Audit output routes policy evidence to official URL index",
|
|
401
401
|
"reviewed distribution patch naver-robots-rfc9309"
|
package/source-lock.json
CHANGED
|
@@ -395,7 +395,7 @@
|
|
|
395
395
|
"package_path": "skills/optbrew/modules/naver-searchadvisor/scripts/audit.mjs",
|
|
396
396
|
"original_git_blob_sha": "de8c4a3430633cb2445e339d7a943d944cbf45a9",
|
|
397
397
|
"original_sha256": "3fd83141a00a9d45d4fbffa9d8ff515c7a49e7b1ea4637844e92375ea7797ff9",
|
|
398
|
-
"distributed_sha256": "
|
|
398
|
+
"distributed_sha256": "fa70051e07b429ad9477b8d778232cc4b3ca0bae8d2a1352074dfd4cb6f5f884",
|
|
399
399
|
"transformations": [
|
|
400
400
|
"Audit output routes policy evidence to official URL index",
|
|
401
401
|
"reviewed distribution patch naver-robots-rfc9309"
|