optbrew 0.1.1 → 0.1.3

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.
@@ -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 { declaredRobotsIndex } from './collector.mjs';
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) {
@@ -154,12 +178,32 @@ const pathWithin = (value, prefix, host) => {
154
178
  return wanted === '/' || actual === wanted || actual.startsWith(wanted.endsWith('/') ? wanted : `${wanted}/`);
155
179
  };
156
180
 
181
+ /**
182
+ * A failed fetch has no response, but its answered redirect hops are real requests. They are replayed from the hashed
183
+ * failure record, which also says whether one more attempt got no response.
184
+ */
185
+ function boundFailure(artifact, audit, requireRef) {
186
+ const entry = (audit.failures ?? []).find(x => x.normalized_url === artifact.normalized_url);
187
+ if (!artifact.failure_ref || !entry || entry.evidence_ref !== artifact.failure_ref) throw new ContractError('failure record missing for failed fetch');
188
+ const raw = requireRef(artifact.failure_ref, 'optbrew:collector');
189
+ if (raw.sha256 !== entry.content_hash) throw new ContractError('failure record hash differs from the audit index');
190
+ const record = JSON.parse(raw.text);
191
+ // At most ten redirects are followed, and the eleventh answer (a refused redirect) is the last hop that can be recorded.
192
+ if (record.kind !== 'failure' || ![0, 1].includes(record.unanswered) || !Array.isArray(record.redirect_chain) || record.redirect_chain.length > 11 ||
193
+ record.redirect_chain.some((hop, index) => typeof hop?.url !== 'string' || !(hop.status >= 300 && hop.status <= 399) ||
194
+ hop.url !== (index ? record.redirect_chain[index - 1].location : record.normalized_url)))
195
+ throw new ContractError('failure record is malformed');
196
+ for (const key of ['requested_url', 'normalized_url', 'redirect_chain', 'unanswered', 'error'])
197
+ if (!same(record[key], artifact[key])) throw new ContractError(`failure record differs from collection record: ${key}`);
198
+ return { ...artifact, ...record };
199
+ }
200
+
157
201
  /**
158
202
  * Replay a collected response from its hashed record: final URL, redirect chain and header lines come from the
159
203
  * record, and the collection entry must agree with it. Bodyless failures have no response to bind.
160
204
  */
161
205
  function boundArtifact(artifact, audit, requireRef) {
162
- if (!Number.isInteger(artifact.status)) return artifact;
206
+ if (!Number.isInteger(artifact.status)) return boundFailure(artifact, audit, requireRef);
163
207
  const entry = (audit.responses ?? []).find(x => x.normalized_url === artifact.normalized_url);
164
208
  if (!artifact.response_ref || !entry || entry.evidence_ref !== artifact.response_ref)
165
209
  throw new ContractError('response record missing for collected artifact');
@@ -327,6 +371,75 @@ function statusFor(finding, changes, afterInput, target, requireRef) {
327
371
  return linked.every(change => change.environment === 'local') && localResolved ? 'verified_local' : 'modified_unverified';
328
372
  }
329
373
 
374
+ /**
375
+ * The request ledger accounts for every queued or rejected URL. Collection counts and the skipped list must equal what
376
+ * the ledger and the hashed response records show, so a fetched page cannot be moved to skipped and a request limit
377
+ * cannot be claimed without the requests that exhaust it.
378
+ */
379
+ function checkLedger(input, audit, collection, artifacts, requireRef) {
380
+ const ledger = JSON.parse(requireRef(audit.ledger_ref).text);
381
+ const entries = ledger.entries;
382
+ const fail = message => { throw new ContractError(`request ledger: ${message}`); };
383
+ if (!Array.isArray(entries)) fail('entries missing');
384
+ const limit = Math.min(input.run_context.scope?.max_urls ?? 50, 50);
385
+ if (ledger.limit !== limit) fail('request limit differs from the run scope');
386
+ const total = entries.reduce((sum, entry) => sum + entry.requests, 0);
387
+ // The limit is a hard ceiling: every request, redirect hops included, takes one slot before it is sent.
388
+ if (total > limit) fail('more requests than the limit');
389
+ // One URL, one entry. Skips for out-of-scope and undeclared auxiliary links are recorded per link and may repeat.
390
+ const identities = entries.filter(x => !(x.outcome === 'skipped' && ['outside_scope', 'auxiliary_undeclared'].includes(x.skipped_reason))).map(x => x.url);
391
+ if (new Set(identities).size !== identities.length) fail('duplicate ledger url: a URL appears in more than one entry');
392
+ const responded = artifacts.filter(x => Number.isInteger(x.status));
393
+ const failed = artifacts.filter(x => !Number.isInteger(x.status));
394
+ const byUrl = outcome => new Map(entries.filter(x => x.outcome === outcome).map(x => [x.url, x]));
395
+ const responses = byUrl('response'), errors = byUrl('error');
396
+ if (responses.size !== responded.length || audit.responses.length !== responded.length || errors.size !== failed.length || audit.failures.length !== failed.length)
397
+ fail('collected artifacts differ from the ledger or the response records');
398
+ for (const artifact of responded) {
399
+ const entry = responses.get(artifact.normalized_url);
400
+ if (!entry || entry.requests !== artifact.redirect_chain.length + 1) fail(`response for ${artifact.normalized_url} is not recorded`);
401
+ }
402
+ // A skipped entry never reached the network. Requests booked on it would only pad the total toward a claimed limit.
403
+ if (entries.some(x => x.outcome === 'skipped' && x.requests !== 0)) fail('a skipped entry carries requests');
404
+ for (const artifact of failed) {
405
+ const entry = errors.get(artifact.normalized_url);
406
+ if (!entry) fail(`failed fetch of ${artifact.normalized_url} is not recorded`);
407
+ // The requests of a failed fetch are the answered hops of its hashed failure record plus at most one attempt that got
408
+ // no response; nothing in collection.json can raise them.
409
+ if (entry.requests !== artifact.redirect_chain.length + artifact.unanswered) fail(`failed fetch of ${artifact.normalized_url} does not match its hashed failure record`);
410
+ }
411
+ const key = x => `${x.url}|${x.reason ?? x.skipped_reason}`;
412
+ const ledgerSkips = entries.filter(x => x.skipped_reason).map(key).sort();
413
+ if (JSON.stringify((collection.skipped ?? []).map(key).sort()) !== JSON.stringify(ledgerSkips)) fail('skipped list differs from the ledger');
414
+ const limited = entries.some(x => ['url_limit', 'request_limit'].includes(x.skipped_reason));
415
+ if (collection.limit_reached !== limited) fail('limit_reached differs from the ledger');
416
+ if (limited && total !== limit) fail('a request limit is claimed but the recorded requests do not use the whole limit');
417
+ if (collection.request_count !== total || collection.evaluated_count !== entries.filter(x => x.requests > 0).length) fail('request or evaluated count differs from the ledger');
418
+ const selected = new Set(input.run_context.protocol.page_types.flatMap(type => type.urls.map(x => x.normalized)));
419
+ 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');
420
+ // The protocol caps selected pages plus auxiliary resources at the limit, and the collector queues them before any
421
+ // discovered page. A selected page can only hit the request limit after redirects used the budget, so no page that was
422
+ // merely discovered may have been fetched in the same run.
423
+ const auxiliary = new Set((input.run_context.protocol.auxiliary_resources ?? []).map(x => x.normalized));
424
+ const planned = new Set([...selected, ...auxiliary]);
425
+ // Provenance: a failed fetch or a request-limit skip belongs to a URL the run was given (selected or auxiliary) or one
426
+ // that a hashed page of this collection links to (anchors and canonical, as the collector follows them). A URL nothing
427
+ // links to cannot be a failed fetch or a skipped one, so fabricated entries cannot pad the total or claim a limit.
428
+ const known = new Set(planned);
429
+ for (const artifact of responded) {
430
+ if (artifact.status !== 200 || artifact.capability !== 'raw_html' || auxiliary.has(artifact.normalized_url)) continue;
431
+ for (const link of links(requireRef(artifact.evidence_ref).text, artifact.final_url)) {
432
+ try { known.add(assertUrlInScope(link, input.run_context.scope).href); } catch { /* out of scope: the collector skipped it */ }
433
+ }
434
+ }
435
+ for (const entry of entries) {
436
+ if ((entry.outcome === 'error' || (entry.outcome === 'skipped' && entry.skipped_reason === 'url_limit')) && !known.has(entry.url))
437
+ fail(`${entry.url} has no provenance: it is neither a selected or auxiliary URL nor linked from a collected page`);
438
+ }
439
+ if (entries.some(x => selected.has(x.url) && x.outcome === 'skipped' && x.skipped_reason === 'url_limit') &&
440
+ 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');
441
+ }
442
+
330
443
  function checkAuditPhase(input, phase, snapshot, requireRef) {
331
444
  if (!input) return;
332
445
  const observations = input.observations.filter(x => x.source_ref === 'optbrew:collector');
@@ -341,6 +454,10 @@ function checkAuditPhase(input, phase, snapshot, requireRef) {
341
454
  const savedObservations = JSON.parse(requireRef(audit.observations_ref).text);
342
455
  if (!Array.isArray(collection.artifacts) || !Array.isArray(audit.raw) || !Array.isArray(savedObservations))
343
456
  throw new ContractError('audit snapshot files incomplete');
457
+ if (!Array.isArray(audit.modules) || !audit.ledger_ref || !Array.isArray(audit.responses) || !Array.isArray(audit.failures))
458
+ throw new ContractError('audit evidence was produced by an older optbrew release (it lacks the request ledger, module list or failure records); re-run the audit with optbrew 0.1.3 and build a new packet');
459
+ for (const name of audit.modules) if (!Array.isArray(moduleOutputs[name])) throw new ContractError(`planned module output missing: ${name}`);
460
+ const naverPlanned = audit.modules.includes('naver-searchadvisor');
344
461
  const rawRefs = new Set();
345
462
  for (const raw of audit.raw) {
346
463
  if (raw.content_hash === null) {
@@ -355,6 +472,7 @@ function checkAuditPhase(input, phase, snapshot, requireRef) {
355
472
  }
356
473
  // Final URL, redirect chain and header lines come from the hashed response records, not from collection.json alone.
357
474
  const artifacts = collection.artifacts.map(x => boundArtifact(x, audit, requireRef));
475
+ checkLedger(input, audit, collection, artifacts, requireRef);
358
476
  for (const obs of observations) {
359
477
  if (!savedObservations.some(x => same(x, obs)) || !rawRefs.has(obs.evidence_ref))
360
478
  throw new ContractError('collector observation is not bound to audit index and raw evidence');
@@ -374,21 +492,20 @@ function checkAuditPhase(input, phase, snapshot, requireRef) {
374
492
  if (['index-policy', 'document-title'].includes(obs.check_id) &&
375
493
  (['pass', 'fail'].includes(obs.state) || obs.check_id === 'document-title' && obs.state === 'unknown')) {
376
494
  const raw = artifact && requireRef(obs.evidence_ref);
377
- if (!module || !artifact || artifact.status !== 200 || artifact.capability !== 'raw_html' ||
495
+ if ((naverPlanned && !module) || !artifact || artifact.status !== 200 || artifact.capability !== 'raw_html' ||
378
496
  artifact.raw_html_ref !== obs.evidence_ref || artifact.content_hash !== raw.sha256 || raw.kind !== 'raw_html')
379
497
  throw new ContractError('title/index verdict lacks bound raw response');
380
498
  const replay = auditCollected({ artifact, html: raw.text, engines });
381
- if (!same([module.title, module.robots_meta, module.x_robots_tag],
499
+ if (naverPlanned && (!same([module.title, module.robots_meta, module.x_robots_tag],
382
500
  [replay.title, replay.robots_meta, replay.x_robots_tag]) ||
383
501
  (module.requested_url !== undefined && module.requested_url !== artifact.normalized_url) ||
384
- (module.redirect_chain !== undefined && !same(module.redirect_chain, artifact.redirect_chain ?? [])))
502
+ (module.redirect_chain !== undefined && !same(module.redirect_chain, artifact.redirect_chain ?? []))))
385
503
  throw new ContractError('title/index module differs from raw response and headers');
386
- // A present title proves existence only; uniqueness and descriptiveness need separate evidence.
387
- const expected = obs.check_id === 'document-title' ? replay.title ? 'unknown' : 'fail' : replay.index_blocked ? 'fail' : 'pass';
388
- if (obs.state !== expected)
504
+ // Title states (missing, duplicated, unknown) are replayed by the derivation below.
505
+ if (obs.check_id === 'index-policy' && obs.state !== (replay.index_blocked ? 'fail' : 'pass'))
389
506
  throw new ContractError('title/index verdict differs from raw response and headers');
390
507
  }
391
- if (obs.check_id === 'search-crawl-policy' && ['pass', 'fail'].includes(obs.state)) {
508
+ if (obs.check_id === 'search-crawl-policy' && ['pass', 'fail'].includes(obs.state) && obs.unit_key.split('|')[1] === 'naver') {
392
509
  const [, engine] = obs.unit_key.split('|');
393
510
  const robotsArtifact = artifacts.find(x => x.evidence_ref === obs.evidence_ref && x.status === 200);
394
511
  const declared = robotsArtifact && (input.run_context.protocol.auxiliary_resources ?? []).some(resource =>
@@ -409,40 +526,26 @@ function checkAuditPhase(input, phase, snapshot, requireRef) {
409
526
  throw new ContractError('crawl verdict differs from bound robots evidence and module output');
410
527
  }
411
528
  }
412
- // Every observation the collector plans for this phase must be in the packet and in the saved observation file;
413
- // deleting a failed unit from both would otherwise silently remove its blocker.
414
- if (!Array.isArray(moduleOutputs['naver-searchadvisor'])) return;
415
- const planned = new Set(input.run_context.protocol.families.filter(x => x.applicable).map(x => x.family_id));
416
- const pages = input.run_context.protocol.page_types.flatMap(type => type.urls);
417
- const declaredRobots = new Set((input.run_context.protocol.auxiliary_resources ?? [])
418
- .filter(resource => resource.kind === 'robots').map(resource => resource.normalized));
419
- const expect = (family, engine, url) => {
420
- const unitKey = `${family}|${engine ? `${engine}|` : ''}${url}`;
421
- if (!observations.some(x => x.unit_key === unitKey) || !savedObservations.some(x => x.unit_key === unitKey))
422
- throw new ContractError(`planned collector observation missing: ${unitKey}`);
423
- };
424
- for (const { normalized: url, public_indexing: publicIndexing } of pages) {
425
- const artifact = artifacts.find(x => x.normalized_url === url);
426
- if (!artifact) {
427
- // A selected page is either collected or recorded as skipped; vanishing from both hides its observations.
428
- if (!(collection.skipped ?? []).some(x => x.url === url))
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
- }
529
+ // Every selected page is either collected or recorded as skipped; vanishing from both hides its observations.
530
+ for (const { normalized: url } of input.run_context.protocol.page_types.flatMap(type => type.urls))
531
+ if (!artifacts.some(x => x.normalized_url === url) && !(collection.skipped ?? []).some(x => x.url === url))
532
+ throw new ContractError(`selected page missing from the collection and its skipped list: ${url}`);
533
+ // Re-derive every collector verdict from the hashed raw evidence. Each derived observation must be present in the
534
+ // packet and in the saved observation file, and no collector observation may lack a derivation.
535
+ const derived = deriveObservations({ context: input.run_context, artifacts, collection,
536
+ textOf: artifact => artifact.content_hash ? requireRef(artifact.evidence_ref).text : null });
537
+ const shape = x => [x.state, x.q, x.partial_basis, x.evidence_ref, x.reason, x.evaluator_type, x.source_ref];
538
+ for (const expected of derived) {
539
+ if (!observations.some(x => x.unit_key === expected.unit_key) || !savedObservations.some(x => x.unit_key === expected.unit_key))
540
+ throw new ContractError(`planned collector observation missing: ${expected.unit_key}`);
541
+ const have = observations.find(x => x.unit_key === expected.unit_key);
542
+ if (!same(shape(have), shape(expected)))
543
+ throw new ContractError(expected.family_id === 'search-crawl-policy' ? 'crawl verdict differs from bound robots evidence and module output' :
544
+ ['index-policy', 'document-title'].includes(expected.family_id) ? 'title/index verdict differs from raw response and headers' :
545
+ `collector verdict differs from raw evidence: ${expected.unit_key}`);
445
546
  }
547
+ for (const have of observations) if (!derived.some(x => x.unit_key === have.unit_key))
548
+ throw new ContractError(`collector observation is not derivable from raw evidence: ${have.unit_key}`);
446
549
  }
447
550
 
448
551
  /** Recompute scores and metrics from contained run evidence; render four design tables. */
@@ -599,11 +702,18 @@ export function buildReport(packet) {
599
702
  const scoreRows = baselineScores.map(before => {
600
703
  const after = byProfile.get(before.profile);
601
704
  const delta = comparison.score_deltas.find(x => x.profile === before.profile);
602
- return [before.profile, before.score, after?.score ?? 'unknown', delta?.percentage_point_delta ?? 'unknown',
705
+ const cautious = scoreHeadline(before).status !== 'sufficient' || (after && scoreHeadline(after).status !== 'sufficient');
706
+ return [before.profile, headlineCell(before), after ? headlineCell(after) : 'unknown',
707
+ `${delta?.percentage_point_delta ?? 'unknown'}${cautious && delta ? ' (between measured subsets)' : ''}`,
603
708
  `${before.coverage ?? 'unknown'} → ${after?.coverage ?? 'unknown'}`,
604
709
  `${before.missing_bounds?.join('–') ?? 'unknown'} → ${after?.missing_bounds?.join('–') ?? 'unknown'}`,
605
710
  `${before.readiness_status} → ${after?.readiness_status ?? 'unknown'}`];
606
711
  });
712
+ const headlines = baselineScores.map(scoreHeadline);
713
+ result.headline = headlines;
714
+ const headlineText = headlines.map(h => h.status === 'sufficient' ? `${h.profile}: ${h.headline_score} (coverage ${h.coverage}%)` :
715
+ 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'}` :
716
+ `${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
717
  const remediationRows = findings.map(f => [f.priority, `${f.message} ${f.unit_key}`, f.sources.map(x => `${x.source_ref} ${x.evidence_ref}`).join('; '),
608
718
  f.remediation ? `${f.remediation.mechanism} (${f.remediation.source_url}, checked ${f.remediation.checked_date})` : 'unknown',
609
719
  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 +724,7 @@ export function buildReport(packet) {
614
724
  const markdown = `# optbrew report\n\n${packet.data_class === 'synthetic' ? '**Synthetic example — no real site or provider outcome.**\n\n' : ''}` +
615
725
  `Target: ${html(packet.run_context.target)} \nEnvironment: ${html(packet.run_context.environment)} \nGoals: ${html(packet.run_context.goals.join(', '))} \n` +
616
726
  `Comparison: ${html(comparison.status)} (${html(comparison.reasons.join(', ') || 'none')}) \n` +
727
+ `Headline: \n${headlineText.map(line => `- ${html(line)} \n`).join('')}` +
617
728
  `Baseline blockers: ${html(baselineScores[0]?.blockers.length ?? 0)} (${html(baselineScores[0]?.blockers.map(x => `${x.family_id} ${x.unit_key}`).join('; ') || 'none')}) → ` +
618
729
  `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
730
  `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": "616ea1e0e9295bd3f4c53374d36ce14dcb5923650c94c63c3d4032590fb93a8a",
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": "616ea1e0e9295bd3f4c53374d36ce14dcb5923650c94c63c3d4032590fb93a8a",
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"