@runbooks/search 0.1.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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/bm25.d.ts +67 -0
- package/dist/bm25.js +73 -0
- package/dist/cursor.d.ts +45 -0
- package/dist/cursor.js +49 -0
- package/dist/endpoint.d.ts +53 -0
- package/dist/endpoint.js +103 -0
- package/dist/endpoint.test.d.ts +1 -0
- package/dist/endpoint.test.js +182 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/log.d.ts +45 -0
- package/dist/log.js +58 -0
- package/dist/search.d.ts +108 -0
- package/dist/search.js +111 -0
- package/dist/search.test.d.ts +1 -0
- package/dist/search.test.js +253 -0
- package/dist/stack.d.ts +92 -0
- package/dist/stack.js +164 -0
- package/package.json +36 -0
package/dist/stack.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack matching (W-20, §17, §13.3).
|
|
3
|
+
*
|
|
4
|
+
* §17's third entry point taken to its conclusion: a reader says what they have — targets
|
|
5
|
+
* and versions, the capabilities their client offers, how much risk they will accept, what
|
|
6
|
+
* supervision they enforce — and the catalog answers what applies to *them*.
|
|
7
|
+
*
|
|
8
|
+
* The privacy shape is the constraint that decides the design. **A declared stack is an
|
|
9
|
+
* infrastructure fingerprint**: postgres 14.3 and kubernetes 1.29 and a private MCP server
|
|
10
|
+
* names an organization more precisely than most of what §14 refuses to collect in a run
|
|
11
|
+
* report. So the declaration never travels. This module is a pure function over the
|
|
12
|
+
* published index, run in the reader's browser, and there is nothing here that could send
|
|
13
|
+
* one anywhere — no client, no logger, no callback.
|
|
14
|
+
*
|
|
15
|
+
* The matching itself is deliberately unclever. A range that cannot be parsed does not
|
|
16
|
+
* match rather than matching hopefully: telling somebody a procedure applies to their
|
|
17
|
+
* cluster when nobody checked is worse than telling them nothing.
|
|
18
|
+
*/
|
|
19
|
+
import { DEFAULT_TRUST_MIN } from "./search.js";
|
|
20
|
+
const RISK_ORDER = ["read-only", "reversible-write", "destructive", "irreversible"];
|
|
21
|
+
const PROFILE_ORDER = ["R0", "R1", "R2"];
|
|
22
|
+
const TRUST_ORDER = ["T0", "T1", "T2", "T3", "T4"];
|
|
23
|
+
/**
|
|
24
|
+
* Does this version satisfy this range?
|
|
25
|
+
*
|
|
26
|
+
* A deliberately small subset of what a version range can be: comparators over dotted
|
|
27
|
+
* numbers, joined by spaces meaning "and". Anything else is **unparseable, and unparseable
|
|
28
|
+
* does not match** — a range this cannot read is a question nobody answered, and answering
|
|
29
|
+
* it optimistically puts a procedure in front of somebody whose cluster it was never
|
|
30
|
+
* checked against.
|
|
31
|
+
*/
|
|
32
|
+
export function satisfies(version, range) {
|
|
33
|
+
const parts = range.trim().split(/\s+/).filter(Boolean);
|
|
34
|
+
if (parts.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
let verdict = true;
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
const match = /^(>=|<=|>|<|=)?(\d+(?:\.\d+){0,2})$/.exec(part);
|
|
39
|
+
if (!match)
|
|
40
|
+
return undefined;
|
|
41
|
+
const operator = match[1] ?? "=";
|
|
42
|
+
const compared = compare(version, match[2]);
|
|
43
|
+
const ok = operator === ">=" ? compared >= 0 :
|
|
44
|
+
operator === "<=" ? compared <= 0 :
|
|
45
|
+
operator === ">" ? compared > 0 :
|
|
46
|
+
operator === "<" ? compared < 0 :
|
|
47
|
+
compared === 0;
|
|
48
|
+
verdict = verdict && ok;
|
|
49
|
+
}
|
|
50
|
+
return verdict;
|
|
51
|
+
}
|
|
52
|
+
function compare(a, b) {
|
|
53
|
+
const left = a.split(".").map(Number);
|
|
54
|
+
const right = b.split(".").map(Number);
|
|
55
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
56
|
+
const difference = (left[i] ?? 0) - (right[i] ?? 0);
|
|
57
|
+
if (difference !== 0)
|
|
58
|
+
return difference;
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether a record applies to a stack, and when it does not, why.
|
|
64
|
+
*
|
|
65
|
+
* The refusals are named because "no results" is the least useful thing a catalog can say
|
|
66
|
+
* to somebody who has just described their environment: a reader who is told *their client
|
|
67
|
+
* only enforces R0* knows what to change, and a reader shown an empty list concludes the
|
|
68
|
+
* catalog is empty.
|
|
69
|
+
*/
|
|
70
|
+
export function applicability(record, stack) {
|
|
71
|
+
const declared = Object.keys(stack.targets);
|
|
72
|
+
const matchedTargets = record.targets.filter((target) => {
|
|
73
|
+
if (!declared.includes(target))
|
|
74
|
+
return false;
|
|
75
|
+
const range = record.applies_to?.[target];
|
|
76
|
+
const version = stack.targets[target];
|
|
77
|
+
if (!range || !version)
|
|
78
|
+
return true;
|
|
79
|
+
// Unparseable or unsatisfied both fail: see `satisfies`.
|
|
80
|
+
return satisfies(version, range) === true;
|
|
81
|
+
});
|
|
82
|
+
if (declared.length > 0 && matchedTargets.length === 0) {
|
|
83
|
+
const versioned = record.targets.filter((target) => declared.includes(target));
|
|
84
|
+
if (versioned.length > 0) {
|
|
85
|
+
const ranges = versioned
|
|
86
|
+
.map((target) => `${target} ${record.applies_to?.[target] ?? "any"}`)
|
|
87
|
+
.join(", ");
|
|
88
|
+
return {
|
|
89
|
+
applies: false,
|
|
90
|
+
why: `Your versions are outside what this declares: it applies to ${ranges}.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
applies: false,
|
|
95
|
+
why: `It is for ${record.targets.join(", ") || "no declared target"}, which is not in your stack.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* §13.3, and the one refusal that is about safety rather than relevance: a client that
|
|
100
|
+
* enforces R0 is not shown a procedure demanding R2. Ranked low, it would still be
|
|
101
|
+
* clicked; refused, it says what the reader would have to change.
|
|
102
|
+
*/
|
|
103
|
+
const needs = record.min_runtime_profile ?? "R0";
|
|
104
|
+
const enforces = stack.runtimeProfile ?? "R0";
|
|
105
|
+
if (PROFILE_ORDER.indexOf(needs) > PROFILE_ORDER.indexOf(enforces)) {
|
|
106
|
+
return {
|
|
107
|
+
applies: false,
|
|
108
|
+
why: `It requires ${needs} supervision and your client enforces ${enforces}. This is a refusal rather than a low rank: a procedure that outruns its supervisor is the failure the execution contract exists to prevent.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const missing = record.capabilities.filter((capability) => !stack.capabilities.includes(capability));
|
|
112
|
+
if (missing.length > 0) {
|
|
113
|
+
return {
|
|
114
|
+
applies: false,
|
|
115
|
+
why: `You have not offered ${missing.join(", ")}. Every capability a procedure declares has to be available, or it stops halfway — and part of a destructive procedure is a state nobody designed.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (stack.riskCeiling && RISK_ORDER.indexOf(record.risk) > RISK_ORDER.indexOf(stack.riskCeiling)) {
|
|
119
|
+
return {
|
|
120
|
+
applies: false,
|
|
121
|
+
why: `Its highest risk is ${record.risk} and you set a ceiling of ${stack.riskCeiling}.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (stack.execution && record.execution && stack.execution !== record.execution) {
|
|
125
|
+
return { applies: false, why: `It is ${record.execution} and you asked for ${stack.execution}.` };
|
|
126
|
+
}
|
|
127
|
+
const floor = stack.trustMin ?? DEFAULT_TRUST_MIN;
|
|
128
|
+
if (TRUST_ORDER.indexOf(record.trust) < TRUST_ORDER.indexOf(floor)) {
|
|
129
|
+
return {
|
|
130
|
+
applies: false,
|
|
131
|
+
why: stack.trustMin
|
|
132
|
+
? `It is ${record.trust} and you set a floor of ${stack.trustMin}.`
|
|
133
|
+
: `It is ${record.trust}, below the ${DEFAULT_TRUST_MIN} this shows by default. Lower the floor to see it.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { applies: true, matchedTargets };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* What applies, ranked by applicability and then by trust.
|
|
140
|
+
*
|
|
141
|
+
* Applicability first because relevance is what was asked for; trust second because
|
|
142
|
+
* between two procedures that both fit, the one somebody has verified is the better
|
|
143
|
+
* answer. Ties break on the ref, so two builds of the same catalog agree.
|
|
144
|
+
*/
|
|
145
|
+
export function matchStack(records, stack) {
|
|
146
|
+
return records
|
|
147
|
+
.map((record) => ({ record, verdict: applicability(record, stack) }))
|
|
148
|
+
.filter((entry) => entry.verdict.applies)
|
|
149
|
+
.map((entry) => ({ record: entry.record, matchedTargets: entry.verdict.matchedTargets }))
|
|
150
|
+
.sort((a, b) => {
|
|
151
|
+
if (a.matchedTargets.length !== b.matchedTargets.length) {
|
|
152
|
+
return b.matchedTargets.length - a.matchedTargets.length;
|
|
153
|
+
}
|
|
154
|
+
const trust = TRUST_ORDER.indexOf(b.record.trust) - TRUST_ORDER.indexOf(a.record.trust);
|
|
155
|
+
return trust !== 0 ? trust : a.record.ref < b.record.ref ? -1 : 1;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** Everything that did not apply, with the reason: an empty list explains nothing. */
|
|
159
|
+
export function excluded(records, stack) {
|
|
160
|
+
return records
|
|
161
|
+
.map((record) => ({ record, verdict: applicability(record, stack) }))
|
|
162
|
+
.filter((entry) => !entry.verdict.applies)
|
|
163
|
+
.map((entry) => ({ ref: entry.record.ref, why: entry.verdict.why }));
|
|
164
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runbooks/search",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Search over the prebuilt index: symptom, facet and capability.",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/runbooks-directory/runbooks.directory"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.11"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -b",
|
|
33
|
+
"test": "vitest run --passWithNoTests",
|
|
34
|
+
"lint": "eslint src"
|
|
35
|
+
}
|
|
36
|
+
}
|