optbrew 0.1.3 → 0.2.0
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/AGENT_INSTALL.md +13 -11
- package/CHANGELOG.md +52 -0
- package/README.ko.md +40 -15
- package/README.md +40 -15
- package/THIRD_PARTY_NOTICES.md +88 -0
- package/docs/platforms.md +10 -10
- package/docs/release.md +37 -5
- package/lib/cli.mjs +5 -2
- package/lib/install.mjs +2 -1
- package/lib/records.mjs +2 -1
- package/lib/transaction.mjs +12 -5
- package/lib/update.mjs +3 -2
- package/package.json +6 -4
- package/plugin.json +2 -2
- package/skills/.claude-plugin/icon.png +0 -0
- package/skills/.claude-plugin/plugin.json +23 -0
- package/skills/README.md +27 -0
- package/skills/optbrew/SKILL.md +223 -8
- package/skills/optbrew/THIRD_PARTY_NOTICES.md +88 -0
- package/skills/optbrew/bundle-manifest.json +407 -47
- package/skills/optbrew/modules/naver-searchadvisor/scripts/audit.mjs +4 -4
- package/skills/optbrew/references/api.md +5 -3
- package/skills/optbrew/references/check-registry.json +16 -16
- package/skills/optbrew/references/chrome-paths.json +31 -0
- package/skills/optbrew/references/endpoints.json +10 -0
- package/skills/optbrew/references/harness-signals.json +5 -0
- package/skills/optbrew/references/intake.md +76 -5
- package/skills/optbrew/references/judging.md +233 -0
- package/skills/optbrew/references/limits.json +26 -0
- package/skills/optbrew/references/measurement.md +1 -1
- package/skills/optbrew/references/messages.ko.json +170 -0
- package/skills/optbrew/references/report.md +18 -6
- package/skills/optbrew/references/routing.md +2 -2
- package/skills/optbrew/references/runbooks/anthropic-api-key.md +34 -0
- package/skills/optbrew/references/runbooks/chrome-install.md +56 -0
- package/skills/optbrew/references/runbooks/cwv-site-collection.md +38 -0
- package/skills/optbrew/references/runbooks/google-api-key.md +51 -0
- package/skills/optbrew/references/runbooks/gsc-export.md +32 -0
- package/skills/optbrew/references/runbooks/index.json +18 -0
- package/skills/optbrew/references/runbooks/key-set-claude-cloud.md +32 -0
- package/skills/optbrew/references/runbooks/key-set-codex.md +33 -0
- package/skills/optbrew/references/runbooks/key-set-local.md +40 -0
- package/skills/optbrew/references/runbooks/naver-searchadvisor-export.md +32 -0
- package/skills/optbrew/references/runbooks/network-claude-cloud-custom.md +33 -0
- package/skills/optbrew/references/runbooks/network-codex-approval.md +26 -0
- package/skills/optbrew/references/runbooks/network-codex-config.md +36 -0
- package/skills/optbrew/references/runbooks/seranking.md +38 -0
- package/skills/optbrew/references/schemas/contract.schema.json +42 -15
- package/skills/optbrew/references/scoring.md +40 -5
- package/skills/optbrew/references/source-analysis.md +40 -0
- package/skills/optbrew/references/versions.json +9 -0
- package/skills/optbrew/scripts/adapters/anthropic.mjs +5 -5
- package/skills/optbrew/scripts/adapters/imports.mjs +138 -1
- package/skills/optbrew/scripts/audit.mjs +10 -112
- package/skills/optbrew/scripts/cli.mjs +5 -2
- package/skills/optbrew/scripts/core/bundle.mjs +30 -13
- package/skills/optbrew/scripts/core/capabilities.mjs +8 -4
- package/skills/optbrew/scripts/core/cdp.mjs +105 -0
- package/skills/optbrew/scripts/core/checks/canonical-consistency.mjs +56 -0
- package/skills/optbrew/scripts/core/checks/cwv-field.mjs +77 -0
- package/skills/optbrew/scripts/core/checks/discovery.mjs +43 -0
- package/skills/optbrew/scripts/core/checks/document-title.mjs +62 -0
- package/skills/optbrew/scripts/core/checks/entity-identity.mjs +31 -0
- package/skills/optbrew/scripts/core/checks/fact-consistency.mjs +31 -0
- package/skills/optbrew/scripts/core/checks/fetch-response.mjs +18 -0
- package/skills/optbrew/scripts/core/checks/index-policy.mjs +16 -0
- package/skills/optbrew/scripts/core/checks/index.mjs +18 -0
- package/skills/optbrew/scripts/core/checks/internal-navigation.mjs +67 -0
- package/skills/optbrew/scripts/core/checks/locale-links.mjs +55 -0
- package/skills/optbrew/scripts/core/checks/naver-markup.mjs +21 -0
- package/skills/optbrew/scripts/core/checks/observation.mjs +37 -0
- package/skills/optbrew/scripts/core/checks/prepare.mjs +103 -0
- package/skills/optbrew/scripts/core/checks/render-content.mjs +104 -0
- package/skills/optbrew/scripts/core/checks/search-crawl-policy.mjs +29 -0
- package/skills/optbrew/scripts/core/checks/structured-data-truth.mjs +46 -0
- package/skills/optbrew/scripts/core/chrome-temp.mjs +68 -0
- package/skills/optbrew/scripts/core/chrome.mjs +546 -0
- package/skills/optbrew/scripts/core/collector.mjs +45 -49
- package/skills/optbrew/scripts/core/compare.mjs +18 -1
- package/skills/optbrew/scripts/core/config.mjs +31 -0
- package/skills/optbrew/scripts/core/contracts.mjs +31 -2
- package/skills/optbrew/scripts/core/cwv.mjs +100 -0
- package/skills/optbrew/scripts/core/derive.mjs +10 -186
- package/skills/optbrew/scripts/core/evidence-store.mjs +112 -0
- package/skills/optbrew/scripts/core/facts.mjs +175 -17
- package/skills/optbrew/scripts/core/findings.mjs +19 -1
- package/skills/optbrew/scripts/core/html.mjs +41 -3
- package/skills/optbrew/scripts/core/index-targets.mjs +118 -0
- package/skills/optbrew/scripts/core/judgments.mjs +675 -0
- package/skills/optbrew/scripts/core/keys.mjs +86 -0
- package/skills/optbrew/scripts/core/link-graph.mjs +93 -0
- package/skills/optbrew/scripts/core/messages.mjs +23 -0
- package/skills/optbrew/scripts/core/metrics.mjs +4 -2
- package/skills/optbrew/scripts/core/migration.mjs +37 -0
- package/skills/optbrew/scripts/core/page-select.mjs +136 -0
- package/skills/optbrew/scripts/core/probe.mjs +83 -0
- package/skills/optbrew/scripts/core/protocol.mjs +38 -14
- package/skills/optbrew/scripts/core/render.mjs +160 -0
- package/skills/optbrew/scripts/core/report-compose.mjs +377 -0
- package/skills/optbrew/scripts/core/report-html.mjs +198 -0
- package/skills/optbrew/scripts/core/report-page.mjs +14 -0
- package/skills/optbrew/scripts/core/report.mjs +327 -122
- package/skills/optbrew/scripts/core/runbooks.mjs +63 -0
- package/skills/optbrew/scripts/core/scope.mjs +23 -5
- package/skills/optbrew/scripts/core/score.mjs +12 -4
- package/skills/optbrew/scripts/core/secrets.mjs +2 -0
- package/skills/optbrew/scripts/core/stages.mjs +694 -0
- package/skills/optbrew/scripts/core/transport.mjs +157 -0
- package/skills/optbrew/scripts/core/weights.mjs +2 -1
- package/skills/optbrew/scripts/key.mjs +90 -0
- package/skills/optbrew/scripts/preflight.mjs +176 -0
- package/skills/optbrew/scripts/run.mjs +81 -0
- package/skills/optbrew/source-lock.json +1 -1
- package/source-lock.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const levels = ['beginner', 'brief'];
|
|
4
|
+
const placeholder = '{{skill_root}}';
|
|
5
|
+
const dir = new URL('../../references/runbooks/', import.meta.url);
|
|
6
|
+
|
|
7
|
+
let cachedIndex;
|
|
8
|
+
const index = () => cachedIndex ??= JSON.parse(readFileSync(new URL('index.json', dir), 'utf8'));
|
|
9
|
+
const entryOf = id => index().find(entry => entry.id === id);
|
|
10
|
+
/** One index entry (`id`, `kind`, `when`, `fills`, `item`, and sometimes `line` and `expands_to`), or undefined. */
|
|
11
|
+
export const runbookEntry = entryOf;
|
|
12
|
+
/** The `harness:` value in a runbook's front matter. */
|
|
13
|
+
const harnessOf = id => parse(id, readFileSync(new URL(`${id}.md`, dir), 'utf8')).head.harness;
|
|
14
|
+
|
|
15
|
+
/** Split a runbook file into its front-matter fields and its `## <name>` sections. */
|
|
16
|
+
function parse(id, raw) {
|
|
17
|
+
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(raw.replace(/\r\n/g, '\n'));
|
|
18
|
+
if (!match) throw new Error(`runbook ${id}: front matter is missing`);
|
|
19
|
+
const head = Object.fromEntries(match[1].split('\n').map(line => [line.slice(0, line.indexOf(':')), line.slice(line.indexOf(':') + 1).trim()]));
|
|
20
|
+
const sections = Object.fromEntries(match[2].split(/^## /m).slice(1).map(part => {
|
|
21
|
+
const newline = part.indexOf('\n');
|
|
22
|
+
return [part.slice(0, newline).trim(), part.slice(newline + 1).trim()];
|
|
23
|
+
}));
|
|
24
|
+
return { head, sections };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One stored runbook at one guidance level; `{{skill_root}}` becomes the caller's skill folder, inserted as given. */
|
|
28
|
+
export function loadRunbook(id, level, { skillRoot } = {}) {
|
|
29
|
+
const entry = entryOf(id);
|
|
30
|
+
if (!entry) throw new Error(`unknown runbook: ${id}`);
|
|
31
|
+
if (!levels.includes(level)) throw new Error(`runbook level must be one of ${levels.join(', ')}: ${level}`);
|
|
32
|
+
const { head, sections } = parse(id, readFileSync(new URL(`${id}.md`, dir), 'utf8'));
|
|
33
|
+
const body = sections[level];
|
|
34
|
+
if (!body) throw new Error(`runbook ${id} has no ${level} section`);
|
|
35
|
+
if (body.includes(placeholder) && !skillRoot) throw new Error(`runbook ${id} needs skillRoot`);
|
|
36
|
+
return { id, kind: entry.kind, text: body.replaceAll(placeholder, () => skillRoot), checked: head.checked, source: head.source, harness: head.harness };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What the start probe says is missing. Owner exports cannot be probed, so they always count as missing. */
|
|
40
|
+
const missingOf = probe => new Set([!probe.google_key && 'google_key', !probe.chrome?.found && 'chrome', 'owner_export'].filter(Boolean));
|
|
41
|
+
|
|
42
|
+
const matchesStart = (when, probe, missing) =>
|
|
43
|
+
(when.block_reason !== undefined && when.block_reason === probe.block_reason) || (when.missing !== undefined && missing.has(when.missing));
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The runbooks to show before the audit starts. A blocking runbook (at most one) comes whole at the chosen level;
|
|
47
|
+
* an improving one comes as its single line with its `harness`. An entry with no `block_reason` or `missing` is never shown here.
|
|
48
|
+
* `harnesses`, when given, lists the runbook `harness` values that apply; others are dropped before the one-blocking cap.
|
|
49
|
+
*/
|
|
50
|
+
export function startRunbooks(probe, { level, skillRoot, harnesses = null }) {
|
|
51
|
+
const missing = missingOf(probe);
|
|
52
|
+
const hits = index().filter(entry => matchesStart(entry.when, probe, missing))
|
|
53
|
+
.map(entry => ({ ...entry, harness: harnessOf(entry.id) })).filter(entry => !harnesses || harnesses.includes(entry.harness));
|
|
54
|
+
const blocking = hits.filter(entry => entry.kind === 'blocking').slice(0, 1).map(entry => loadRunbook(entry.id, level, { skillRoot }));
|
|
55
|
+
const improving = hits.filter(entry => entry.kind === 'improving').map(({ id, line, harness }) => ({ id, line, harness }));
|
|
56
|
+
return { blocking, improving };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Runbook ids for the `unknown` observations whose reason a runbook can fix, in index order, each once. */
|
|
60
|
+
export function remainingRunbooks(observations) {
|
|
61
|
+
const reasons = new Set(observations.filter(observation => observation.state === 'unknown').map(observation => observation.reason));
|
|
62
|
+
return index().filter(entry => reasons.has(entry.when.unknown_reason)).map(entry => entry.id);
|
|
63
|
+
}
|
|
@@ -40,13 +40,31 @@ export function assertUrlInScope(input, scope) {
|
|
|
40
40
|
return url;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
export const unbracket = host => host.replace(/^\[|\]$/g, '');
|
|
44
|
+
|
|
45
|
+
/** Host without IPv6 brackets and the port the URL connects to. */
|
|
46
|
+
export const urlTarget = url => ({ host: unbracket(url.hostname), port: Number(url.port || (url.protocol === 'https:' ? 443 : 80)) });
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check origin, path segment, and every resolved address before a request.
|
|
50
|
+
* Through a proxy (`addressVetting: 'proxy'`) the proxy resolves names, so only DNS-name vetting is skipped:
|
|
51
|
+
* IP literals and localhost names are still checked here.
|
|
52
|
+
*/
|
|
53
|
+
export function assertInScope(input, scope, resolvedAddresses, { addressVetting = 'direct' } = {}) {
|
|
45
54
|
const url = assertUrlInScope(input, scope);
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
55
|
+
const target = urlTarget(url);
|
|
56
|
+
const { port } = target;
|
|
57
|
+
// A trailing dot is the same name (`localhost.` resolves like `localhost`), so compare without it.
|
|
58
|
+
const host = target.host.replace(/\.$/, '');
|
|
49
59
|
const local = (scope.local_targets ?? []).some(x => x.host === host && Number(x.port) === port);
|
|
60
|
+
if (addressVetting === 'proxy') {
|
|
61
|
+
if (local) return url;
|
|
62
|
+
if (isIP(host)) {
|
|
63
|
+
if (privateAddress(host)) throw new Error('private or reserved address outside local target');
|
|
64
|
+
} else if (host === 'localhost' || host.endsWith('.localhost')) throw new Error('localhost outside local target');
|
|
65
|
+
return url;
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(resolvedAddresses) || !resolvedAddresses.length) throw new Error('resolved addresses required');
|
|
50
68
|
for (const address of resolvedAddresses) if (privateAddress(address) && !local) throw new Error('private or reserved address outside local target');
|
|
51
69
|
return url;
|
|
52
70
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { validateEntity, cwvVerdict, ContractError } from './contracts.mjs';
|
|
4
|
+
import { versions } from './config.mjs';
|
|
4
5
|
import { compileUnits, scoreWeighted } from './weights.mjs';
|
|
6
|
+
import { assertCurrentEvidence } from './migration.mjs';
|
|
5
7
|
|
|
6
8
|
const registry = JSON.parse(readFileSync(new URL('../../references/check-registry.json', import.meta.url)));
|
|
7
9
|
const profiles = JSON.parse(readFileSync(new URL('../../references/profiles.json', import.meta.url)));
|
|
8
10
|
const bundleRoot = new URL('../../', import.meta.url);
|
|
11
|
+
// Same rule as fileDigest in bundle.mjs: CRLF from a core.autocrlf checkout hashes as the committed LF bytes.
|
|
12
|
+
const sourceDigest = bytes => createHash('sha256')
|
|
13
|
+
.update(bytes.includes(0) ? bytes : Buffer.from(bytes.toString('latin1').replaceAll('\r\n', '\n'), 'latin1')).digest('hex');
|
|
9
14
|
|
|
10
15
|
function assertEvaluationIdentity(context) {
|
|
11
16
|
const manifest = JSON.parse(readFileSync(new URL('bundle-manifest.json', bundleRoot), 'utf8'));
|
|
@@ -17,7 +22,7 @@ function assertEvaluationIdentity(context) {
|
|
|
17
22
|
const sources = manifest[field];
|
|
18
23
|
if (!Array.isArray(sources) || !sources.length ||
|
|
19
24
|
createHash('sha256').update(JSON.stringify(sources)).digest('hex') !== manifest[hashField] ||
|
|
20
|
-
sources.some(source =>
|
|
25
|
+
sources.some(source => sourceDigest(readFileSync(new URL(source.path, bundleRoot))) !== source.sha256))
|
|
21
26
|
throw new ContractError(`${field} identity differs from installed bundle`);
|
|
22
27
|
}
|
|
23
28
|
}
|
|
@@ -34,6 +39,7 @@ function band(score) {
|
|
|
34
39
|
|
|
35
40
|
/** Readiness scores are local audit status, never predicted search or AI outcomes. */
|
|
36
41
|
export function score(input) {
|
|
42
|
+
assertCurrentEvidence(input, 'score input');
|
|
37
43
|
validateEntity('ScoreInput', input);
|
|
38
44
|
assertEvaluationIdentity(input.run_context);
|
|
39
45
|
const units = compileUnits(input, 'SEO');
|
|
@@ -61,11 +67,13 @@ export function score(input) {
|
|
|
61
67
|
const selectedProfiles = Object.keys(profiles.profiles).filter(name => name !== 'NEO' || input.run_context.protocol.engines.includes('naver'));
|
|
62
68
|
return selectedProfiles.map(profile => {
|
|
63
69
|
const weighted = scoreWeighted(compileUnits(input, profile));
|
|
64
|
-
const
|
|
70
|
+
const readiness = weighted.A === 0 ? 'not_applicable' : blockers.length ? 'blocked' :
|
|
65
71
|
unknown_core.length ? 'unverified' : 100 * weighted.M / weighted.A < 80 - 1e-9 ? 'provisional' : 'measured';
|
|
66
|
-
|
|
72
|
+
// Verdicts of a phase with no audit cannot be replayed from collected evidence, so such a phase is never 'measured'.
|
|
73
|
+
const readiness_status = readiness === 'measured' && input.run_context.audited !== true ? 'unverified' : readiness;
|
|
74
|
+
const result = { schema_version: versions().contract_schema, evaluation_version: input.run_context.evaluation_version,
|
|
67
75
|
profile, ...weighted, band: band(weighted.M === 0 ? null : 100 * weighted.N / weighted.M), readiness_status,
|
|
68
|
-
blockers, unknown_core, source_ref:
|
|
76
|
+
blockers, unknown_core, source_ref: `optbrew:scoring:${versions().evaluation}` };
|
|
69
77
|
return validateEntity('ScoreResult', result);
|
|
70
78
|
});
|
|
71
79
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Secret and private-identifier shapes that must never reach a report packet, stored evidence, or a model-judgment queue. */
|
|
2
|
+
export const sensitive = /(?:sk-ant-api03-[A-Za-z0-9_-]{32,}|sk-proj-[A-Za-z0-9_-]{32,}|ghp_[A-Za-z0-9]{36,}|AKIA[A-Z0-9]{16}|AIza[0-9A-Za-z_-]{35}|\b\d{6}-[1-8]\d{6}\b)/;
|