@panaversity/ksor 0.0.51 → 0.0.52
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/CHANGELOG.md +58 -0
- package/dist/cli.mjs +172 -4
- package/dist/{gateway-api-D8HlLys2-Ca8OnwLn.mjs → gateway-api-Ck1l_Sgs-B-_wMYLB.mjs} +18 -11
- package/dist/gateway.mjs +1 -1
- package/docs/ingesting.md +47 -0
- package/docs/tool-surface.md +25 -10
- package/package.json +3 -3
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +16 -1
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +16 -1
- package/templates/scaffold/.ksor/people.yaml +32 -0
- package/templates/scaffold/AGENTS.md +47 -1
- package/templates/scaffold/gitignore +1 -0
- package/templates/scaffold/system/site/components/governance.tsx +6 -4
- package/templates/scaffold/system/site/lib/actor-display.ts +53 -0
- package/templates/scaffold/system/site/lib/people.ts +82 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# @panaversity/ksor
|
|
2
2
|
|
|
3
|
+
## 0.0.52
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1e60b9d: The site prints natural names for the actors a record cites, where the record
|
|
8
|
+
declares them.
|
|
9
|
+
|
|
10
|
+
A governed page led with a slug — "Owner · human:bashiraziz" — on every owner,
|
|
11
|
+
trust, approval and withdrawal line. `.ksor/people.yaml` maps an actor to the
|
|
12
|
+
name a page should print, and the site substitutes it at render time. An actor
|
|
13
|
+
with no entry renders exactly as stored: no splitting, no camelCase guessing,
|
|
14
|
+
no derivation. A display name is the one thing in a governance line that cannot
|
|
15
|
+
be computed — `bashiraziz` is equally "Bashir Aziz" and "Bashira Ziz" — so the
|
|
16
|
+
owner is its only source.
|
|
17
|
+
|
|
18
|
+
A MAP keyed by the whole identifier, not a list of names a handle is derived
|
|
19
|
+
from. The derivation could only ever match a handle that IS somebody's squashed
|
|
20
|
+
full name, so `human:ciso`, `human:audit-lead` and `human:mjs` — most of the
|
|
21
|
+
actors in a real record — had no expressible name at all; and it collided, since
|
|
22
|
+
two different people can squash to one handle.
|
|
23
|
+
|
|
24
|
+
Deliberately NOT part of `.ksor/governance.yaml`. That file is the root of
|
|
25
|
+
authority: its key set is closed so nothing can sit there without being
|
|
26
|
+
enforced, and its digest is hashed into `build.lock.json` — so a display name
|
|
27
|
+
living there would mean correcting the spelling of somebody's name refused the
|
|
28
|
+
next site build as `ksor-lock-stale`. Appearing in `people.yaml` confers no
|
|
29
|
+
authority; it only changes what is printed, and nothing cross-checks the two
|
|
30
|
+
lists, because a person who leaves the authority list is still the recorded
|
|
31
|
+
approver of everything they approved.
|
|
32
|
+
|
|
33
|
+
- f23cddc: `ksor calibrate --check` reports whether a declared abstention floor is still
|
|
34
|
+
holding, from the record's own traffic.
|
|
35
|
+
|
|
36
|
+
A floor is measured once and the record then grows. As it does, questions that
|
|
37
|
+
used to be out-of-corpus start scoring above a fixed number, so the record
|
|
38
|
+
answers what it used to refuse — no error, nothing logged, and the same
|
|
39
|
+
`gate: { floor: … }` in every envelope. AGENTS.md forbids copying a calibrated
|
|
40
|
+
constant between corpora; the same reasoning applies across time within one
|
|
41
|
+
corpus, and nothing enforced it (#182).
|
|
42
|
+
|
|
43
|
+
It needs no telemetry and no new dependency: every search already leaves an
|
|
44
|
+
audit row carrying the gate's own signal, on both sides of the gate, so this is
|
|
45
|
+
one indexed query — no provider key, no embedding call, no LLM. It reports the
|
|
46
|
+
abstain rate, the percentiles of answered top scores, and how many answers
|
|
47
|
+
landed within 0.01 of the floor (the size of the decision in this project's own
|
|
48
|
+
gold, not a threshold somebody picked).
|
|
49
|
+
|
|
50
|
+
**It never fails a run**, and that is the design rather than a limitation. A
|
|
51
|
+
stale floor wants re-measuring; failing a build for one would make the shortest
|
|
52
|
+
way out deleting `vector_floor` — turning the abstention gate off entirely to
|
|
53
|
+
clear the error, which is the escape `build/lifecycle-notice.ts` refuses to
|
|
54
|
+
create for a passed review date. It is also a monitor and not a measurement: it
|
|
55
|
+
can say a floor has gone permissive against real traffic, never that it is too
|
|
56
|
+
strict for questions nobody asked, and it says so rather than reporting a
|
|
57
|
+
healthy-looking nothing on a record no one queries.
|
|
58
|
+
|
|
59
|
+
- a403e19: the served envelope now discloses when a §7 audit row could not be written (issue #150)
|
|
60
|
+
|
|
3
61
|
## 0.0.51
|
|
4
62
|
|
|
5
63
|
### Patch Changes
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as resolveCommand, r as verbs, t as exitCodes } from "./src-dqpI-p1a.mjs";
|
|
3
|
-
import { A as parseViewer, B as tlsPosture, C as contentPoolMin, D as outlineHandler, E as keyRingFromEnv, F as runProbe$1, H as withPgRetry$1, I as searchHandler, L as servingPolicy$1, M as prewarmPool, N as readHandler, O as parseInstanceText$1, P as recordIsUndescribed, R as storedTextSearchConfig, S as contentPool$1, T as instancePathOf$1, U as withProbeDeadline$1, V as validateViewer, W as z$1, _ as assertGovernanceServable$1, a as GovernanceGateError$1, b as checkEmbeddingSpace$1, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, g as TextSearchConfigMismatch, h as TRUST_TIERS$1, i as FLOOR, j as pooledEndpointFor, k as parseTrustFloor, l as MissingProviderKeyError$1, m as SchemaVersionError, n as ContentStoreError$1, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, r as EmbeddingSpaceMismatch$1, t as AudienceError$1, u as OUTLINE_OUTPUT, v as assertSchemaCompatible, w as embedQueryVlit, x as composeInstructions, y as buildShippedProvider$1, z as tallyHandlers } from "./gateway-api-
|
|
3
|
+
import { A as parseViewer, B as tlsPosture, C as contentPoolMin, D as outlineHandler, E as keyRingFromEnv, F as runProbe$1, H as withPgRetry$1, I as searchHandler, L as servingPolicy$1, M as prewarmPool, N as readHandler, O as parseInstanceText$1, P as recordIsUndescribed, R as storedTextSearchConfig, S as contentPool$1, T as instancePathOf$1, U as withProbeDeadline$1, V as validateViewer, W as z$1, _ as assertGovernanceServable$1, a as GovernanceGateError$1, b as checkEmbeddingSpace$1, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, g as TextSearchConfigMismatch, h as TRUST_TIERS$1, i as FLOOR, j as pooledEndpointFor, k as parseTrustFloor, l as MissingProviderKeyError$1, m as SchemaVersionError, n as ContentStoreError$1, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, r as EmbeddingSpaceMismatch$1, t as AudienceError$1, u as OUTLINE_OUTPUT, v as assertSchemaCompatible, w as embedQueryVlit, x as composeInstructions, y as buildShippedProvider$1, z as tallyHandlers } from "./gateway-api-Ck1l_Sgs-B-_wMYLB.mjs";
|
|
4
4
|
import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { InMemoryTransport, LATEST_PROTOCOL_VERSION, createMcpHandler } from "@modelcontextprotocol/server";
|
|
@@ -17,7 +17,7 @@ import { Document, YAMLParseError, isCollection, isMap, isPair, isSeq, parseAllD
|
|
|
17
17
|
import { parseArgs } from "node:util";
|
|
18
18
|
import { readFile } from "node:fs/promises";
|
|
19
19
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
20
|
-
//#region ../content-gateway/dist/main-
|
|
20
|
+
//#region ../content-gateway/dist/main-mQtKKwyj.mjs
|
|
21
21
|
/**
|
|
22
22
|
* The default registration — and the ORIGINAL of the file `ksor init` emits.
|
|
23
23
|
*
|
|
@@ -4709,7 +4709,7 @@ async function withPgRetry(op, options = {}) {
|
|
|
4709
4709
|
throw lastError;
|
|
4710
4710
|
}
|
|
4711
4711
|
//#endregion
|
|
4712
|
-
//#region ../content/dist/commands-
|
|
4712
|
+
//#region ../content/dist/commands-Cb63_n_A.mjs
|
|
4713
4713
|
/**
|
|
4714
4714
|
* EVAL-LOCKED constants, quarried verbatim from the oracle
|
|
4715
4715
|
* (sor-agentfactory @ b554f91, config.py) — changing any of these is a
|
|
@@ -9743,6 +9743,122 @@ function pct(fraction) {
|
|
|
9743
9743
|
return `${(fraction * 100).toFixed(0)}%`;
|
|
9744
9744
|
}
|
|
9745
9745
|
/**
|
|
9746
|
+
* How close to the floor an answer has to be to count as marginal.
|
|
9747
|
+
*
|
|
9748
|
+
* 0.01 is not a tuned threshold — it is the size of the decision in this
|
|
9749
|
+
* record's own gold, quoted rather than invented: in-corpus at 0.730 / 0.671
|
|
9750
|
+
* against a scope-adjacent near-miss at 0.683 (`evals/behavioural.db.test.ts`),
|
|
9751
|
+
* so a hundredth of a cosine is the smallest difference that has ever changed
|
|
9752
|
+
* an answer here. An answer inside that band is one the floor barely admitted.
|
|
9753
|
+
*/
|
|
9754
|
+
const MARGIN_BAND = .01;
|
|
9755
|
+
/** Nearest-rank percentile over a sorted ascending list. */
|
|
9756
|
+
function percentile(sorted, q) {
|
|
9757
|
+
if (sorted.length === 0) return null;
|
|
9758
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1))] ?? null;
|
|
9759
|
+
}
|
|
9760
|
+
/**
|
|
9761
|
+
* Characterise the traffic against the declared floor.
|
|
9762
|
+
*
|
|
9763
|
+
* Two numbers carry the verdict, and both are reported whatever it says,
|
|
9764
|
+
* because the verdict is a reading aid and the numbers are the evidence:
|
|
9765
|
+
*
|
|
9766
|
+
* the ABSTAIN RATE — a gate that has stopped refusing anything is
|
|
9767
|
+
* either serving a record that now covers its
|
|
9768
|
+
* traffic, or a floor that has fallen behind it.
|
|
9769
|
+
* the MARGINAL SHARE — answers the floor barely admitted. These are the
|
|
9770
|
+
* ones that would flip if the number moved at all,
|
|
9771
|
+
* so a large share means the answer set turns on a
|
|
9772
|
+
* constant measured against a smaller corpus.
|
|
9773
|
+
*
|
|
9774
|
+
* `watch` is deliberately not called "stale". This cannot tell the two causes
|
|
9775
|
+
* apart — only a re-measurement can — so it names what it saw and says what to
|
|
9776
|
+
* run.
|
|
9777
|
+
*/
|
|
9778
|
+
function driftReport(floor, samples) {
|
|
9779
|
+
const answeredScores = samples.filter((s) => !s.abstained).map((s) => s.topCosine).sort((a, b) => a - b);
|
|
9780
|
+
const abstained = samples.length - answeredScores.length;
|
|
9781
|
+
const marginal = answeredScores.filter((score) => score - floor <= .010000001).length;
|
|
9782
|
+
const marginalShare = answeredScores.length === 0 ? 0 : marginal / answeredScores.length;
|
|
9783
|
+
const abstainRate = samples.length === 0 ? 0 : abstained / samples.length;
|
|
9784
|
+
const base = {
|
|
9785
|
+
floor,
|
|
9786
|
+
samples: samples.length,
|
|
9787
|
+
abstained,
|
|
9788
|
+
answered: answeredScores.length,
|
|
9789
|
+
marginal,
|
|
9790
|
+
marginalShare,
|
|
9791
|
+
abstainRate,
|
|
9792
|
+
p05: percentile(answeredScores, .05),
|
|
9793
|
+
p50: percentile(answeredScores, .5),
|
|
9794
|
+
p95: percentile(answeredScores, .95)
|
|
9795
|
+
};
|
|
9796
|
+
if (samples.length < 30) return {
|
|
9797
|
+
...base,
|
|
9798
|
+
verdict: "no-data",
|
|
9799
|
+
why: `only ${samples.length} logged search(es) — too few to characterise; this reports traffic, so a record nobody queries says nothing rather than looking healthy`
|
|
9800
|
+
};
|
|
9801
|
+
if (marginalShare >= .2) return {
|
|
9802
|
+
...base,
|
|
9803
|
+
verdict: "watch",
|
|
9804
|
+
why: `${marginal} of ${answeredScores.length} answers scored within ${MARGIN_BAND} of the floor — the answer set turns on this exact number`
|
|
9805
|
+
};
|
|
9806
|
+
if (abstained === 0) return {
|
|
9807
|
+
...base,
|
|
9808
|
+
verdict: "watch",
|
|
9809
|
+
why: `the gate refused none of ${samples.length} searches — either the record now covers its traffic, or the floor has fallen behind it, and only a re-measurement tells them apart`
|
|
9810
|
+
};
|
|
9811
|
+
return {
|
|
9812
|
+
...base,
|
|
9813
|
+
verdict: "steady",
|
|
9814
|
+
why: `${abstained} of ${samples.length} searches were refused, and ${marginal} answer(s) sat within ${MARGIN_BAND} of the floor`
|
|
9815
|
+
};
|
|
9816
|
+
}
|
|
9817
|
+
/** The report as the CLI prints it. Numbers first; the verdict is a reading aid. */
|
|
9818
|
+
function renderDrift(report, window) {
|
|
9819
|
+
const pct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
9820
|
+
const score = (n) => n === null ? "—" : n.toFixed(3);
|
|
9821
|
+
const lines = [
|
|
9822
|
+
`floor drift — ${window}`,
|
|
9823
|
+
` declared vector_floor ${report.floor.toFixed(3)}`,
|
|
9824
|
+
` searches logged ${report.samples} (${report.answered} answered, ${report.abstained} abstained)`,
|
|
9825
|
+
` abstain rate ${pct(report.abstainRate)}`,
|
|
9826
|
+
` answered top score p05 ${score(report.p05)} p50 ${score(report.p50)} p95 ${score(report.p95)}`,
|
|
9827
|
+
` within ${MARGIN_BAND} of floor ${report.marginal} (${pct(report.marginalShare)} of answers)`,
|
|
9828
|
+
` verdict ${report.verdict.toUpperCase()} — ${report.why}`
|
|
9829
|
+
];
|
|
9830
|
+
if (report.verdict === "watch") lines.push("", " This is traffic, not a calibration: it cannot tell a record that grew", " from a floor that fell behind. Re-measure to find out —", " ksor calibrate --instance instance.md --queries-file <your questions>", " and paste the floor it prints if it differs from the declared one.");
|
|
9831
|
+
return `${lines.join("\n")}\n`;
|
|
9832
|
+
}
|
|
9833
|
+
/**
|
|
9834
|
+
* The logged searches for this corpus, newest `days` days.
|
|
9835
|
+
*
|
|
9836
|
+
* Only rows carrying a NUMERIC `top_cosine`: an audit row shed under
|
|
9837
|
+
* saturation, or one written before that detail existed, is ABSENT rather than
|
|
9838
|
+
* counted as a zero — a shed row is a gap in the evidence, and scoring it as
|
|
9839
|
+
* zero would drag every statistic here toward a number nobody measured, in the
|
|
9840
|
+
* direction that makes a floor look safer than it is.
|
|
9841
|
+
*
|
|
9842
|
+
* Scoped by CORPUS as well as tenant, for the reason `readLedger` records: a
|
|
9843
|
+
* tenant serving two corpora would otherwise measure one record's floor
|
|
9844
|
+
* against the other's traffic. Read through `runAuditRead` — the serving role
|
|
9845
|
+
* has no SELECT on this table at all, deliberately, and widening that to read
|
|
9846
|
+
* a monitor would trade an audit guarantee for a convenience.
|
|
9847
|
+
*/
|
|
9848
|
+
const DRIFT_SQL = `
|
|
9849
|
+
SELECT (detail->>'top_cosine')::float8 AS top_cosine,
|
|
9850
|
+
action = 'search_abstained' AS abstained
|
|
9851
|
+
FROM retrieval_log
|
|
9852
|
+
WHERE tenant_id = $1
|
|
9853
|
+
AND corpus_id = $2
|
|
9854
|
+
AND action IN ('similarity_searched','search_abstained')
|
|
9855
|
+
AND created_at > now() - ($3 || ' days')::interval
|
|
9856
|
+
AND jsonb_typeof(detail->'top_cosine') = 'number'
|
|
9857
|
+
ORDER BY created_at DESC
|
|
9858
|
+
LIMIT $4`;
|
|
9859
|
+
/** How many rows one check reads at most — a bound, so a busy record cannot make this expensive. */
|
|
9860
|
+
const DRIFT_LIMIT = 5e3;
|
|
9861
|
+
/**
|
|
9746
9862
|
* Out-of-corpus probes scoring at or above the weakest in-corpus question,
|
|
9747
9863
|
* worst first — the ones that decided the verdict. Empty when the measurement
|
|
9748
9864
|
* separated, because then nothing held it open.
|
|
@@ -9939,9 +10055,14 @@ Usage:
|
|
|
9939
10055
|
when the tree is in a repository; --source-commit overrides it.
|
|
9940
10056
|
ksor calibrate --instance PATH [--queries-file PATH] [--ooc-file PATH]
|
|
9941
10057
|
[--generation N] [--per-node N] [--min-chars N]
|
|
10058
|
+
ksor calibrate --instance PATH --check [--days N]
|
|
9942
10059
|
Measure the abstention floor for this corpus and report it. A
|
|
9943
10060
|
measurement that does not separate in-corpus from out-of-corpus prints
|
|
9944
10061
|
the diagnosis and NO floor: there is no safe number to paste.
|
|
10062
|
+
--check reads the record's OWN logged searches instead and reports how
|
|
10063
|
+
the declared floor is holding against them — no provider key, no
|
|
10064
|
+
embedding call, no LLM. A monitor, never a gate: it says what to
|
|
10065
|
+
re-measure and always exits 0.
|
|
9945
10066
|
ksor grant --instance PATH [--revoke]
|
|
9946
10067
|
Authorize ingest for the instance's tenant (the row row-level security
|
|
9947
10068
|
requires), or withdraw it. Idempotent; reports the state it established.
|
|
@@ -10292,13 +10413,19 @@ async function calibrateCommand(args) {
|
|
|
10292
10413
|
"ooc-file": { type: "string" },
|
|
10293
10414
|
generation: { type: "string" },
|
|
10294
10415
|
"per-node": { type: "string" },
|
|
10295
|
-
"min-chars": { type: "string" }
|
|
10416
|
+
"min-chars": { type: "string" },
|
|
10417
|
+
check: {
|
|
10418
|
+
type: "boolean",
|
|
10419
|
+
default: false
|
|
10420
|
+
},
|
|
10421
|
+
days: { type: "string" }
|
|
10296
10422
|
}
|
|
10297
10423
|
});
|
|
10298
10424
|
const instance = loadInstance(values.instance);
|
|
10299
10425
|
if (typeof instance === "number") return instance;
|
|
10300
10426
|
const dsn = resolveDsn(instance);
|
|
10301
10427
|
if (typeof dsn === "number") return dsn;
|
|
10428
|
+
if (values.check === true) return await checkFloorDrift(instance, dsn, values.days);
|
|
10302
10429
|
const provider = composeProvider(instance);
|
|
10303
10430
|
if (typeof provider === "number") return provider;
|
|
10304
10431
|
let queries = null;
|
|
@@ -10327,6 +10454,47 @@ async function calibrateCommand(args) {
|
|
|
10327
10454
|
if (advice !== null) process.stdout.write(advice);
|
|
10328
10455
|
return 0;
|
|
10329
10456
|
}
|
|
10457
|
+
/** How many days of traffic one --check reads. Bounded so a busy record cannot make it expensive. */
|
|
10458
|
+
const DRIFT_DEFAULT_DAYS = 30;
|
|
10459
|
+
/**
|
|
10460
|
+
* `ksor calibrate --check` — is the declared floor still holding?
|
|
10461
|
+
*
|
|
10462
|
+
* Reads the record's OWN logged searches (`retrieval_log.detail.top_cosine`,
|
|
10463
|
+
* which is written on both sides of the gate) instead of measuring the corpus
|
|
10464
|
+
* again, so it needs no provider key, no embedding call and no LLM. That is
|
|
10465
|
+
* why it runs BEFORE the provider is composed: a check that demanded a vendor
|
|
10466
|
+
* key would be one an adopter never puts in CI.
|
|
10467
|
+
*
|
|
10468
|
+
* ALWAYS EXITS 0. A stale floor wants re-measuring; failing a run for one would
|
|
10469
|
+
* make the shortest way out deleting `vector_floor`, which turns the abstention
|
|
10470
|
+
* gate off entirely to clear the error — the same escape `lifecycle-notice.ts`
|
|
10471
|
+
* refuses to create for a passed review date.
|
|
10472
|
+
*/
|
|
10473
|
+
async function checkFloorDrift(instance, dsn, daysArg) {
|
|
10474
|
+
const floor = instance.abstain.vectorFloor;
|
|
10475
|
+
if (floor === null) {
|
|
10476
|
+
process.stdout.write("floor drift: no floor declared — this record's gate is OFF, so out-of-corpus questions are answered rather than refused.\n fix: run `ksor calibrate` and paste the retrieval block it prints\n");
|
|
10477
|
+
return 0;
|
|
10478
|
+
}
|
|
10479
|
+
if (floor === "uncalibrated") {
|
|
10480
|
+
process.stdout.write("floor drift: vector_floor is `uncalibrated` — the door refuses every search until a measured number replaces it.\n fix: run `ksor calibrate` and paste the retrieval block it prints\n");
|
|
10481
|
+
return 0;
|
|
10482
|
+
}
|
|
10483
|
+
const days = daysArg === void 0 ? DRIFT_DEFAULT_DAYS : intFlag("--days", daysArg);
|
|
10484
|
+
const samples = (await withPool(dsn, (pool) => runAuditRead(pool, instance.tenantId, async (client) => {
|
|
10485
|
+
return (await client.query(DRIFT_SQL, [
|
|
10486
|
+
instance.tenantId,
|
|
10487
|
+
instance.corpusId,
|
|
10488
|
+
String(days),
|
|
10489
|
+
DRIFT_LIMIT
|
|
10490
|
+
])).rows;
|
|
10491
|
+
}))).map((row) => ({
|
|
10492
|
+
topCosine: row.top_cosine,
|
|
10493
|
+
abstained: row.abstained
|
|
10494
|
+
}));
|
|
10495
|
+
process.stdout.write(renderDrift(driftReport(floor, samples), `last ${days} day(s)`));
|
|
10496
|
+
return 0;
|
|
10497
|
+
}
|
|
10330
10498
|
async function grantCommand(args) {
|
|
10331
10499
|
const { values } = parseArgs({
|
|
10332
10500
|
args,
|
|
@@ -6,7 +6,7 @@ import { z, z as z$1 } from "zod";
|
|
|
6
6
|
import path, { join } from "node:path";
|
|
7
7
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
8
8
|
import pg from "pg";
|
|
9
|
-
//#region ../content-gateway/dist/gateway-api-
|
|
9
|
+
//#region ../content-gateway/dist/gateway-api-Ck1l_Sgs.mjs
|
|
10
10
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
11
11
|
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
12
12
|
/**
|
|
@@ -10383,7 +10383,7 @@ async function search(ctx, query, k = 10) {
|
|
|
10383
10383
|
}
|
|
10384
10384
|
if (abstained) {
|
|
10385
10385
|
const generation = hits[0]?.generation;
|
|
10386
|
-
await logRead(ctx.pool, {
|
|
10386
|
+
const audited = await logRead(ctx.pool, {
|
|
10387
10387
|
tenantId: inst.tenantId,
|
|
10388
10388
|
corpusId: inst.corpusId,
|
|
10389
10389
|
actor,
|
|
@@ -10415,7 +10415,8 @@ async function search(ctx, query, k = 10) {
|
|
|
10415
10415
|
hits: [],
|
|
10416
10416
|
snapshot: generation === void 0 ? null : snapshotEnvelope(ctx, generation),
|
|
10417
10417
|
...kNote === void 0 ? {} : { k_note: kNote },
|
|
10418
|
-
...degradedReason === void 0 ? {} : { degraded_reason: degradedReason }
|
|
10418
|
+
...degradedReason === void 0 ? {} : { degraded_reason: degradedReason },
|
|
10419
|
+
...audited ? {} : { audit: "degraded" }
|
|
10419
10420
|
};
|
|
10420
10421
|
}
|
|
10421
10422
|
const budget = Math.min(SEARCH_BUDGET_CHARS, inst.maximumResponseCharacters);
|
|
@@ -10447,7 +10448,7 @@ async function search(ctx, query, k = 10) {
|
|
|
10447
10448
|
});
|
|
10448
10449
|
}
|
|
10449
10450
|
const generation = hits[0]?.generation ?? 0;
|
|
10450
|
-
await logRead(ctx.pool, {
|
|
10451
|
+
const audited = await logRead(ctx.pool, {
|
|
10451
10452
|
tenantId: inst.tenantId,
|
|
10452
10453
|
corpusId: inst.corpusId,
|
|
10453
10454
|
actor,
|
|
@@ -10480,7 +10481,8 @@ async function search(ctx, query, k = 10) {
|
|
|
10480
10481
|
...truncated === 0 ? {} : { note: `${truncated} lower-ranked hit(s) dropped by the response budget — narrow the query or use the read tool` },
|
|
10481
10482
|
...advisory ? { content_advisory: CONTENT_ADVISORY } : {},
|
|
10482
10483
|
...kNote === void 0 ? {} : { k_note: kNote },
|
|
10483
|
-
...degradedReason === void 0 ? {} : { degraded_reason: degradedReason }
|
|
10484
|
+
...degradedReason === void 0 ? {} : { degraded_reason: degradedReason },
|
|
10485
|
+
...audited ? {} : { audit: "degraded" }
|
|
10484
10486
|
};
|
|
10485
10487
|
}
|
|
10486
10488
|
const DOCUMENT_BUDGET_CHARS = 28e4;
|
|
@@ -10580,7 +10582,7 @@ async function readDocument(ctx, slug, options = {}) {
|
|
|
10580
10582
|
const textChars = codePointLength(text);
|
|
10581
10583
|
const totalChars = scoped.reduce((n, c) => n + codePointLength(c.content), 0);
|
|
10582
10584
|
const sections = [...new Set(scoped.map((c) => c.headingPath.split("/")[0] ?? "").filter((s) => s !== ""))];
|
|
10583
|
-
await logRead(ctx.pool, {
|
|
10585
|
+
const audited = await logRead(ctx.pool, {
|
|
10584
10586
|
tenantId: inst.tenantId,
|
|
10585
10587
|
corpusId: inst.corpusId,
|
|
10586
10588
|
actor,
|
|
@@ -10618,7 +10620,8 @@ async function readDocument(ctx, slug, options = {}) {
|
|
|
10618
10620
|
note: window.nextHeading === null ? "windowed — this is the last window (next is null)" : "windowed — continue with from_heading set to this response's next (it carries its own scope; do not also resend heading)"
|
|
10619
10621
|
} : {},
|
|
10620
10622
|
...instructionLike(text) || instructionLike(frontmatter ?? "") ? { content_advisory: CONTENT_ADVISORY } : {},
|
|
10621
|
-
snapshot_status: refreshed ?? (pinned === null ? "unpinned" : "pinned")
|
|
10623
|
+
snapshot_status: refreshed ?? (pinned === null ? "unpinned" : "pinned"),
|
|
10624
|
+
...audited ? {} : { audit: "degraded" }
|
|
10622
10625
|
};
|
|
10623
10626
|
}
|
|
10624
10627
|
async function outlineDocuments(ctx, options = {}) {
|
|
@@ -10642,7 +10645,7 @@ async function outlineDocuments(ctx, options = {}) {
|
|
|
10642
10645
|
}), servingScope(ctx));
|
|
10643
10646
|
const has_more = rows.length > limit;
|
|
10644
10647
|
if (has_more) rows.length = limit;
|
|
10645
|
-
await logRead(ctx.pool, {
|
|
10648
|
+
const audited = await logRead(ctx.pool, {
|
|
10646
10649
|
tenantId: inst.tenantId,
|
|
10647
10650
|
corpusId: inst.corpusId,
|
|
10648
10651
|
actor,
|
|
@@ -10659,6 +10662,7 @@ async function outlineDocuments(ctx, options = {}) {
|
|
|
10659
10662
|
});
|
|
10660
10663
|
return {
|
|
10661
10664
|
...rows.some((r) => instructionLike(r.title) || instructionLike(r.headingPath ?? "")) ? { content_advisory: CONTENT_ADVISORY } : {},
|
|
10665
|
+
...audited ? {} : { audit: "degraded" },
|
|
10662
10666
|
has_more,
|
|
10663
10667
|
limit,
|
|
10664
10668
|
offset,
|
|
@@ -10820,7 +10824,8 @@ const SEARCH_OUTPUT = z.object({
|
|
|
10820
10824
|
note: z.string().optional(),
|
|
10821
10825
|
k_note: z.string().optional(),
|
|
10822
10826
|
degraded_reason: z.string().optional().describe("Why retrieval was degraded. \"embed_unavailable\" = the provider is down and this record gates on a cosine floor, so nothing could be served. \"embed_unavailable_keyword_only\" = the provider is down and this record declares no floor, so these hits come from keyword search alone and rank differently."),
|
|
10823
|
-
content_advisory: z.string().optional()
|
|
10827
|
+
content_advisory: z.string().optional(),
|
|
10828
|
+
audit: z.enum(["degraded"]).optional().describe("Present only when the §7 audit row for this act could not be written (shed under saturation). The answer is unaffected; absent means the row landed normally.")
|
|
10824
10829
|
});
|
|
10825
10830
|
const OUTLINE_OUTPUT = z.object({
|
|
10826
10831
|
nodes: z.array(z.object({
|
|
@@ -10838,7 +10843,8 @@ const OUTLINE_OUTPUT = z.object({
|
|
|
10838
10843
|
limit: z.number().int().describe("Rows this page could hold."),
|
|
10839
10844
|
offset: z.number().int().describe("Rows skipped to produce this page."),
|
|
10840
10845
|
next_offset: z.number().int().nullable().describe("Pass as offset to get the next page; null when this is the last one."),
|
|
10841
|
-
has_more: z.boolean().describe("True when rows were cut at limit — the record has more, this list is partial.")
|
|
10846
|
+
has_more: z.boolean().describe("True when rows were cut at limit — the record has more, this list is partial."),
|
|
10847
|
+
audit: z.enum(["degraded"]).optional().describe("Present only when the §7 audit row for this act could not be written (shed under saturation). The answer is unaffected; absent means the row landed normally.")
|
|
10842
10848
|
});
|
|
10843
10849
|
const READ_OUTPUT = z.object({
|
|
10844
10850
|
slug: z.string(),
|
|
@@ -10856,7 +10862,8 @@ const READ_OUTPUT = z.object({
|
|
|
10856
10862
|
est_tokens: z.number().optional(),
|
|
10857
10863
|
total_est_tokens: z.number().optional(),
|
|
10858
10864
|
note: z.string().optional(),
|
|
10859
|
-
content_advisory: z.string().optional()
|
|
10865
|
+
content_advisory: z.string().optional(),
|
|
10866
|
+
audit: z.enum(["degraded"]).optional().describe("Present only when the §7 audit row for this act could not be written (shed under saturation). The answer is unaffected; absent means the row landed normally.")
|
|
10860
10867
|
});
|
|
10861
10868
|
function toolError(error) {
|
|
10862
10869
|
return {
|
package/dist/gateway.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { D as outlineHandler, I as searchHandler, N as readHandler, P as recordIsUndescribed, W as z$1, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, h as TRUST_TIERS, i as FLOOR, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, s as MAX_SEARCH_K, u as OUTLINE_OUTPUT, x as composeInstructions } from "./gateway-api-
|
|
1
|
+
import { D as outlineHandler, I as searchHandler, N as readHandler, P as recordIsUndescribed, W as z$1, c as McpServer$1, d as READ_ONLY, f as READ_OUTPUT, h as TRUST_TIERS, i as FLOOR, o as MAX_OUTLINE_LIMIT, p as SEARCH_OUTPUT, s as MAX_SEARCH_K, u as OUTLINE_OUTPUT, x as composeInstructions } from "./gateway-api-Ck1l_Sgs-B-_wMYLB.mjs";
|
|
2
2
|
export { FLOOR, MAX_OUTLINE_LIMIT, MAX_SEARCH_K, McpServer$1 as McpServer, OUTLINE_OUTPUT, READ_ONLY, READ_OUTPUT, SEARCH_OUTPUT, TRUST_TIERS, composeInstructions, outlineHandler, readHandler, recordIsUndescribed, searchHandler, z$1 as z };
|
package/docs/ingesting.md
CHANGED
|
@@ -230,6 +230,53 @@ belongs on the in-corpus side — moving it separates the measurement. Sometimes
|
|
|
230
230
|
it is a genuine near-miss the corpus cannot separate, and then the floor
|
|
231
231
|
correctly stays uncalibrated.
|
|
232
232
|
|
|
233
|
+
### The floor goes stale as the record grows
|
|
234
|
+
|
|
235
|
+
A floor is measured once, against the corpus as it stood that day, and then the
|
|
236
|
+
record grows. As it does, questions that used to be out-of-corpus start scoring
|
|
237
|
+
above a fixed number — so the record answers what it used to refuse, with no
|
|
238
|
+
error, nothing logged, and the same `gate: { floor: … }` in every envelope. The
|
|
239
|
+
guarantee weakens in silence, in the dangerous direction.
|
|
240
|
+
|
|
241
|
+
You do not need telemetry to see it. Every search already leaves a row carrying
|
|
242
|
+
the gate's own signal, on both sides of it, so the check reads the record's own
|
|
243
|
+
traffic:
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
pnpm exec ksor calibrate --instance instance.md --check
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
No provider key, no embedding call, no LLM — one indexed query:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
floor drift — last 30 day(s)
|
|
253
|
+
declared vector_floor 0.550
|
|
254
|
+
searches logged 112 (100 answered, 12 abstained)
|
|
255
|
+
abstain rate 10.7%
|
|
256
|
+
answered top score p05 0.552 p50 0.810 p95 0.890
|
|
257
|
+
within 0.01 of floor 40 (40.0% of answers)
|
|
258
|
+
verdict WATCH — 40 of 100 answers scored within 0.01 of the floor
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Two numbers carry it. The **abstain rate**: a gate that has stopped refusing
|
|
262
|
+
anything is either serving a record that now covers its traffic, or a floor that
|
|
263
|
+
has fallen behind it. And the **share of answers within 0.01 of the floor** —
|
|
264
|
+
the ones that would flip if the number moved at all, 0.01 being the size of the
|
|
265
|
+
decision in this project's own gold rather than a threshold somebody picked.
|
|
266
|
+
|
|
267
|
+
Run it on a schedule, or in your own CI beside `ksor build`. Three things to
|
|
268
|
+
know about what it is:
|
|
269
|
+
|
|
270
|
+
- **It never fails a run.** It always exits 0. A stale floor wants
|
|
271
|
+
re-measuring, and failing a build for one would make the shortest way out
|
|
272
|
+
deleting `vector_floor` — turning the gate off entirely to clear the error.
|
|
273
|
+
- **It reads traffic, so it needs traffic**, and it says so rather than
|
|
274
|
+
reporting a healthy-looking nothing. It also cannot see questions nobody
|
|
275
|
+
asked: it can tell you the floor has gone permissive, never that it is too
|
|
276
|
+
strict.
|
|
277
|
+
- **It is not a calibration.** When it says WATCH, re-run `ksor calibrate` —
|
|
278
|
+
that is the measurement, and it is the thing that produces a new number.
|
|
279
|
+
|
|
233
280
|
## Withdrawing a document
|
|
234
281
|
|
|
235
282
|
A takedown is a committed ledger entry FIRST and a database row second, written
|
package/docs/tool-surface.md
CHANGED
|
@@ -22,22 +22,27 @@ numbers are exact for every record:
|
|
|
22
22
|
|
|
23
23
|
| | chars | ~tokens | |
|
|
24
24
|
| ------------------------------ | ------ | ------- | ------------------- |
|
|
25
|
-
| all three, as transmitted |
|
|
26
|
-
| `search` alone |
|
|
27
|
-
| `outline` alone | 3,
|
|
28
|
-
| `read` alone | 5,
|
|
29
|
-
| `outline` + `read`, if deleted |
|
|
25
|
+
| all three, as transmitted | 17,394 | 4,349 | **always resident** |
|
|
26
|
+
| `search` alone | 8,152 | 2,038 | always resident |
|
|
27
|
+
| `outline` alone | 3,552 | 888 | always resident |
|
|
28
|
+
| `read` alone | 5,686 | 1,422 | always resident |
|
|
29
|
+
| `outline` + `read`, if deleted | 9,238 | 2,310 | the saving below |
|
|
30
30
|
|
|
31
31
|
**Two measurements, so read the first row apart from the rest.** Each tool's
|
|
32
32
|
row is the JSON of that tool's own object; the first row is the JSON of the
|
|
33
33
|
whole `tools` array, which carries four characters no tool's row does — its
|
|
34
34
|
two brackets and the two separators between three tools. So the three tools
|
|
35
|
-
sum to **
|
|
35
|
+
sum to **17,390** and the array is **17,394**. Deleting a tool saves that
|
|
36
36
|
tool's own row, not a share of the total.
|
|
37
37
|
|
|
38
|
-
They grew
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
They grew, twice, and each rise is priced rather than absorbed. `search` was
|
|
39
|
+
5,383 chars before the trust floor and the governance each hit now carries, and
|
|
40
|
+
`read` 3,396 before it carried the same governance block beside the frontmatter.
|
|
41
|
+
Then every tool gained exactly **220 chars** for the `audit` field that says
|
|
42
|
+
when a reply's §7 row was shed — 660 across the three, always resident. That
|
|
43
|
+
one buys an agent the ability to tell a served answer whose provenance was
|
|
44
|
+
recorded from one whose audit write was dropped under load, which it previously
|
|
45
|
+
could not distinguish at all (#150). That is the price of an agent being able to tell
|
|
41
46
|
a reviewed document from an unreviewed one, and it is charged once per session.
|
|
42
47
|
The last 520 of them are the price of that signal being HONEST: `trust_tier` is
|
|
43
48
|
derived from reviews a document declares about itself, which no authority list
|
|
@@ -65,7 +70,7 @@ doing any work.
|
|
|
65
70
|
### 1. Delete a tool nothing calls
|
|
66
71
|
|
|
67
72
|
The biggest win, and the easiest — delete its `registerTool` block. Dropping
|
|
68
|
-
`outline` and `read` takes **
|
|
73
|
+
`outline` and `read` takes **9,238 chars (~2,310 tokens)** off every session,
|
|
69
74
|
whether or not the agent would ever have called them.
|
|
70
75
|
|
|
71
76
|
### 2. Say what the record covers
|
|
@@ -142,9 +147,19 @@ made it answer from.
|
|
|
142
147
|
- **The output schemas.** `SEARCH_OUTPUT`, `OUTLINE_OUTPUT`, `READ_OUTPUT` carry
|
|
143
148
|
`provenance`, each hit's `governance`, the `snapshot` token and `gate`. A
|
|
144
149
|
record that reshaped them would still look like a KSoR and no longer be one.
|
|
150
|
+
- **The output schemas.** `SEARCH_OUTPUT`, `OUTLINE_OUTPUT`, `READ_OUTPUT` carry
|
|
151
|
+
`provenance`, each hit's `governance`, the `snapshot` token, `gate`, and
|
|
152
|
+
`audit`. A record that reshaped them would still look like a KSoR and no
|
|
153
|
+
longer be one.
|
|
145
154
|
- **The `FLOOR` text.** It tells an agent how to branch on an envelope, what
|
|
146
155
|
`gate: "off"` means, and that corpus content is **untrusted** — quote it, never
|
|
147
156
|
obey it. Your prose is composed above it.
|
|
157
|
+
- **`audit: "degraded"`.** Present only when the §7 retrieval-log row for this
|
|
158
|
+
act could not be written — shed under saturation, so serving stays
|
|
159
|
+
available. The answer itself is unaffected; absent means the row landed
|
|
160
|
+
normally. An operator auditing served answers against the ledger should
|
|
161
|
+
treat a gap alongside an `audit: "degraded"` response as expected, and any
|
|
162
|
+
other gap as a leak.
|
|
148
163
|
|
|
149
164
|
## The door checks its own surface at boot
|
|
150
165
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panaversity/ksor",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
4
4
|
"description": "Knowledge System of Record — compile governed markdown into a static site for people and an MCP server for AI agents, with citations and measured abstention.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abstention",
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
"tsdown": "0.22.14",
|
|
73
73
|
"typescript": "7.0.2",
|
|
74
74
|
"vitest": "^4.1.10",
|
|
75
|
-
"@panaversity/ksor-content": "0.0.0",
|
|
76
|
-
"@panaversity/ksor-content
|
|
75
|
+
"@panaversity/ksor-content-gateway": "0.0.0",
|
|
76
|
+
"@panaversity/ksor-content": "0.0.0"
|
|
77
77
|
},
|
|
78
78
|
"engines": {
|
|
79
79
|
"node": ">=24"
|
|
@@ -49,6 +49,12 @@ the question — that is what made the last version unanswerable.
|
|
|
49
49
|
**3 of 3 — Who signs off on a document, and who can take one down?**
|
|
50
50
|
|
|
51
51
|
> Names or handles, not job titles. If it is just you, say so.
|
|
52
|
+
>
|
|
53
|
+
> Ask for BOTH: the handle the record will store (`human:bashiraziz`) and the
|
|
54
|
+
> natural name to print ("Bashir Aziz"). They are not derivable from each other
|
|
55
|
+
> — `human:ciso` and `human:mjs` are not anybody's squashed full name — so the
|
|
56
|
+
> handle goes into the frontmatter and the policy, and the pair goes into
|
|
57
|
+
> `.ksor/people.yaml` for the site to print.
|
|
52
58
|
|
|
53
59
|
---
|
|
54
60
|
|
|
@@ -120,7 +126,7 @@ never an email address.
|
|
|
120
126
|
climb, not part of this interview). The strictness answer from question 5
|
|
121
127
|
is the intent behind the `retrieval.vector_floor` on that climb, measured
|
|
122
128
|
by `ksor calibrate` — capture it in the prose now so it is ready.
|
|
123
|
-
- Write `.ksor/governance.yaml` from
|
|
129
|
+
- Write `.ksor/governance.yaml` from question 3: `version: "0.1"`,
|
|
124
130
|
the `audiences:` registry if there is one, and the two authority sets with
|
|
125
131
|
real actors. That file is the root of authority — every approval, every
|
|
126
132
|
deprecation and every ledger entry is checked against it. **Keep
|
|
@@ -128,6 +134,15 @@ never an email address.
|
|
|
128
134
|
document is still in `knowledge/`.** Those five are approved by it, so a
|
|
129
135
|
policy rewritten without it refuses the next build by name
|
|
130
136
|
(`ksor-approver-unauthorised`). It leaves when the last sample does.
|
|
137
|
+
- Write `.ksor/people.yaml` from question 3: `version: "0.1"` and a `people:`
|
|
138
|
+
MAP from each actor to its natural name — `"human:bashiraziz": Bashir Aziz`.
|
|
139
|
+
Keyed by the actor exactly as the record stores it, quoted because it
|
|
140
|
+
contains a colon. Nothing else — the site looks the actor up at render time,
|
|
141
|
+
so pages read "Owner · Bashir Aziz" instead of "Owner · human:bashiraziz". Every skill that records a governance
|
|
142
|
+
act (this one, add-sources for `verified:` entries, `ksor takedown` for
|
|
143
|
+
withdrawals) asks the owner for a natural name whenever it is about to write
|
|
144
|
+
an actor that isn't in `people.yaml` yet — the owner is the only source of a
|
|
145
|
+
display name, never a convention-based guess.
|
|
131
146
|
- **Offer to start replacing the starter documents — they are already
|
|
132
147
|
published.** All five ship `status: stable`, approved by
|
|
133
148
|
`ksor-starter/KSOR-STAMP-VERSION`, so the site and `llms.txt` carry them from
|
|
@@ -49,6 +49,12 @@ the question — that is what made the last version unanswerable.
|
|
|
49
49
|
**3 of 3 — Who signs off on a document, and who can take one down?**
|
|
50
50
|
|
|
51
51
|
> Names or handles, not job titles. If it is just you, say so.
|
|
52
|
+
>
|
|
53
|
+
> Ask for BOTH: the handle the record will store (`human:bashiraziz`) and the
|
|
54
|
+
> natural name to print ("Bashir Aziz"). They are not derivable from each other
|
|
55
|
+
> — `human:ciso` and `human:mjs` are not anybody's squashed full name — so the
|
|
56
|
+
> handle goes into the frontmatter and the policy, and the pair goes into
|
|
57
|
+
> `.ksor/people.yaml` for the site to print.
|
|
52
58
|
|
|
53
59
|
---
|
|
54
60
|
|
|
@@ -120,7 +126,7 @@ never an email address.
|
|
|
120
126
|
climb, not part of this interview). The strictness answer from question 5
|
|
121
127
|
is the intent behind the `retrieval.vector_floor` on that climb, measured
|
|
122
128
|
by `ksor calibrate` — capture it in the prose now so it is ready.
|
|
123
|
-
- Write `.ksor/governance.yaml` from
|
|
129
|
+
- Write `.ksor/governance.yaml` from question 3: `version: "0.1"`,
|
|
124
130
|
the `audiences:` registry if there is one, and the two authority sets with
|
|
125
131
|
real actors. That file is the root of authority — every approval, every
|
|
126
132
|
deprecation and every ledger entry is checked against it. **Keep
|
|
@@ -128,6 +134,15 @@ never an email address.
|
|
|
128
134
|
document is still in `knowledge/`.** Those five are approved by it, so a
|
|
129
135
|
policy rewritten without it refuses the next build by name
|
|
130
136
|
(`ksor-approver-unauthorised`). It leaves when the last sample does.
|
|
137
|
+
- Write `.ksor/people.yaml` from question 3: `version: "0.1"` and a `people:`
|
|
138
|
+
MAP from each actor to its natural name — `"human:bashiraziz": Bashir Aziz`.
|
|
139
|
+
Keyed by the actor exactly as the record stores it, quoted because it
|
|
140
|
+
contains a colon. Nothing else — the site looks the actor up at render time,
|
|
141
|
+
so pages read "Owner · Bashir Aziz" instead of "Owner · human:bashiraziz". Every skill that records a governance
|
|
142
|
+
act (this one, add-sources for `verified:` entries, `ksor takedown` for
|
|
143
|
+
withdrawals) asks the owner for a natural name whenever it is about to write
|
|
144
|
+
an actor that isn't in `people.yaml` yet — the owner is the only source of a
|
|
145
|
+
display name, never a convention-based guess.
|
|
131
146
|
- **Offer to start replacing the starter documents — they are already
|
|
132
147
|
published.** All five ship `status: stable`, approved by
|
|
133
148
|
`ksor-starter/KSOR-STAMP-VERSION`, so the site and `llms.txt` carry them from
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Natural names for the actors this record cites. The site looks an actor up
|
|
2
|
+
# here at render time and prints the name in its place — so a page reads
|
|
3
|
+
# "Owner · Bashir Aziz" instead of "Owner · human:bashiraziz".
|
|
4
|
+
#
|
|
5
|
+
# A MAP, keyed by the actor exactly as the record stores it. Not a derivation:
|
|
6
|
+
# there is no rule that turns "Bashir Aziz" into `mjs`, `ciso` or `audit-lead`,
|
|
7
|
+
# and most handles in a real record are not somebody's squashed full name. The
|
|
8
|
+
# key is the identifier, the value is what to print, and nothing is guessed.
|
|
9
|
+
#
|
|
10
|
+
# people:
|
|
11
|
+
# "human:bashiraziz": Bashir Aziz
|
|
12
|
+
# "human:ciso": Ayesha Khan
|
|
13
|
+
# "team:legal-ops": Legal Operations
|
|
14
|
+
#
|
|
15
|
+
# ONE-WAY. The identifier is what the record STORES, cites, and checks against
|
|
16
|
+
# `.ksor/governance.yaml`; the name here is only what a page prints. Nothing
|
|
17
|
+
# reads this file back into an actor, and no authority follows from appearing
|
|
18
|
+
# in it (which is why it is not part of the policy).
|
|
19
|
+
#
|
|
20
|
+
# NOT the same list as `.ksor/governance.yaml`. That file says who MAY approve
|
|
21
|
+
# or withdraw; this one says what to print for anyone the record cites. An
|
|
22
|
+
# approver is usually in both, for two different reasons; a document's owner and
|
|
23
|
+
# a `verified:` entry's actor are only ever here, because neither carries
|
|
24
|
+
# authority. Nothing cross-checks the two, on purpose: someone who leaves the
|
|
25
|
+
# authority list is still the recorded approver of what they approved, and their
|
|
26
|
+
# name has to keep rendering on those acts.
|
|
27
|
+
#
|
|
28
|
+
# Optional. An actor with no entry renders exactly as stored, as it did before
|
|
29
|
+
# this file existed. The intake and add-sources skills offer to add one; the
|
|
30
|
+
# owner can also edit this file by hand — it is theirs.
|
|
31
|
+
version: "0.1"
|
|
32
|
+
people: {}
|
|
@@ -33,6 +33,36 @@ written by `ksor takedown` — every entry's actor is checked against the
|
|
|
33
33
|
policy, so a line appended by hand is refused exactly as the verb would refuse
|
|
34
34
|
it, and a deleted line is refused by `ksor build` against the file's history.
|
|
35
35
|
|
|
36
|
+
Display names for those actors live in `.ksor/people.yaml`, a small phone book
|
|
37
|
+
mapping each actor to the name a page should print — `"human:bashiraziz": Bashir
|
|
38
|
+
Aziz` — so a page reads "Owner · Bashir Aziz" instead of "Owner ·
|
|
39
|
+
human:bashiraziz". The two are not derivable from each other (`human:ciso` is
|
|
40
|
+
nobody's squashed full name), which is why both are written down.
|
|
41
|
+
|
|
42
|
+
It is a SEPARATE file from `.ksor/governance.yaml` deliberately: the policy is
|
|
43
|
+
the root of authority, its key set is closed so that nothing sits there without
|
|
44
|
+
being enforced, and its digest is hashed into `build.lock.json` — so a display
|
|
45
|
+
name living there would mean correcting the spelling of somebody's name refused
|
|
46
|
+
your next build. Appearing in `people.yaml` confers no authority whatsoever; it
|
|
47
|
+
only changes what is printed.
|
|
48
|
+
|
|
49
|
+
An actor with no entry renders exactly as stored, and the governance skills
|
|
50
|
+
offer to add one when they are about to write an actor the file does not know.
|
|
51
|
+
|
|
52
|
+
**The two files list different things, and neither is derived from the other.**
|
|
53
|
+
`governance.yaml` names who MAY act — an actor must be in `approval_authorities`
|
|
54
|
+
to approve a document, or in `takedown_authorities` to withdraw one.
|
|
55
|
+
`people.yaml` names what to PRINT for any actor the record cites. An approver
|
|
56
|
+
usually appears in both, for two different reasons. A document's `ksor.owner`
|
|
57
|
+
and a `verified:` entry's actor appear only in `people.yaml`, because neither
|
|
58
|
+
carries authority — the policy has no verification family at all. And an
|
|
59
|
+
authority whose handle already reads fine needs no entry at all.
|
|
60
|
+
|
|
61
|
+
Nothing cross-checks them, deliberately: a person who leaves the authority list
|
|
62
|
+
is still the recorded approver of everything they approved, and their name must
|
|
63
|
+
keep rendering on those acts. A governance act is history; the policy is
|
|
64
|
+
current state.
|
|
65
|
+
|
|
36
66
|
## Critical rules
|
|
37
67
|
|
|
38
68
|
1. **The site never contains authored content.** Knowledge goes in
|
|
@@ -215,6 +245,22 @@ Stand it up in this order (each step's errors explain how to fix themselves):
|
|
|
215
245
|
the intent to gate WITHOUT a measurement, and every serve refuses until a
|
|
216
246
|
number replaces it; that is the fail-closed posture, not a starting point.
|
|
217
247
|
|
|
248
|
+
**The same applies across TIME, not only across corpora.** A floor measured
|
|
249
|
+
against 5 documents is a copied constant once the record holds 200, and it
|
|
250
|
+
weakens in silence: questions that used to be out-of-corpus start scoring
|
|
251
|
+
above a fixed number, so the record answers what it used to refuse.
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
pnpm exec ksor calibrate --instance instance.md --check
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
reads the record's own logged searches and reports how the declared floor is
|
|
258
|
+
holding — no provider key, no LLM, one query, and it always exits 0. Run it
|
|
259
|
+
on a schedule; when it says WATCH, re-run `ksor calibrate` to get a new
|
|
260
|
+
number. It is a monitor, not a measurement: it can say the floor has gone
|
|
261
|
+
permissive against real traffic, never that it is too strict for questions
|
|
262
|
+
nobody asked.
|
|
263
|
+
|
|
218
264
|
```sh
|
|
219
265
|
pnpm schema # apply the DDL (once)
|
|
220
266
|
pnpm grant # authorize ingest for this corpus (once)
|
|
@@ -333,7 +379,7 @@ governance block every hit now carries:
|
|
|
333
379
|
|
|
334
380
|
| | |
|
|
335
381
|
| -------------------------------- | ------------------------------ |
|
|
336
|
-
| all three tool definitions | ~4,
|
|
382
|
+
| all three tool definitions | ~4,349 tokens, always resident |
|
|
337
383
|
| one `search` at `k=10` (default) | ~4,196 tokens per call |
|
|
338
384
|
| one `search` at `k=5` | ~2,330 tokens per call |
|
|
339
385
|
|
|
@@ -4,6 +4,8 @@ import { DocumentActions } from "@/components/document-actions";
|
|
|
4
4
|
import { Clock } from "lucide-react";
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
6
|
|
|
7
|
+
import { displayActor } from "@/lib/actor-display";
|
|
8
|
+
import { peopleBook } from "@/lib/people";
|
|
7
9
|
import {
|
|
8
10
|
badgeAddsToStatus,
|
|
9
11
|
badgeText,
|
|
@@ -257,7 +259,7 @@ export function GovernanceMeta({
|
|
|
257
259
|
<Chip text={trust.tier} />
|
|
258
260
|
{trust.by === null ? null : (
|
|
259
261
|
<span className="font-normal text-fd-muted-foreground">
|
|
260
|
-
{trust.by}
|
|
262
|
+
{displayActor(trust.by, peopleBook())}
|
|
261
263
|
{trust.at === null ? null : <> · {day(trust.at)}</>}
|
|
262
264
|
</span>
|
|
263
265
|
)}
|
|
@@ -286,7 +288,7 @@ export function GovernanceMeta({
|
|
|
286
288
|
Links keep full strength: `Replaces` points at the document this one
|
|
287
289
|
superseded, and that is an action rather than a fact. */}
|
|
288
290
|
<dl className="mt-2.5 flex flex-wrap items-baseline gap-x-8 gap-y-2.5 empty:mt-0 [&_a]:text-fd-foreground [&_dd]:font-normal [&_dd]:text-fd-muted-foreground">
|
|
289
|
-
{owner === null ? null : <Fact label="Owner">{owner}</Fact>}
|
|
291
|
+
{owner === null ? null : <Fact label="Owner">{displayActor(owner, peopleBook())}</Fact>}
|
|
290
292
|
{/* Who let this into the record. `ksor.approval` is what makes a `stable`
|
|
291
293
|
document stable at all (record spec §2.2), so a page that showed the
|
|
292
294
|
word and not the signature would be publishing the claim without its
|
|
@@ -294,7 +296,7 @@ export function GovernanceMeta({
|
|
|
294
296
|
{approval === null ? null : (
|
|
295
297
|
<Fact label="Approved">
|
|
296
298
|
<>
|
|
297
|
-
{approval.by} · {day(approval.at)}
|
|
299
|
+
{displayActor(approval.by, peopleBook())} · {day(approval.at)}
|
|
298
300
|
</>
|
|
299
301
|
</Fact>
|
|
300
302
|
)}
|
|
@@ -307,7 +309,7 @@ export function GovernanceMeta({
|
|
|
307
309
|
{deprecated === null ? null : (
|
|
308
310
|
<Fact label="Withdrawn">
|
|
309
311
|
<>
|
|
310
|
-
{deprecated.by} · {day(deprecated.at)}
|
|
312
|
+
{displayActor(deprecated.by, peopleBook())} · {day(deprecated.at)}
|
|
311
313
|
</>
|
|
312
314
|
</Fact>
|
|
313
315
|
)}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a stored actor identifier into the string the page prints.
|
|
3
|
+
*
|
|
4
|
+
* KSoR stores actors as `human:<handle>`, `process:<id>`, `team:<id>` or
|
|
5
|
+
* `<producer>/<version>`. Those forms are right for machines — the checker
|
|
6
|
+
* parses them, `.ksor/governance.yaml` lists actors by them, and the whole
|
|
7
|
+
* authority model depends on them — but a public-facing page shouldn't lead
|
|
8
|
+
* with a slug. This module bridges the two.
|
|
9
|
+
*
|
|
10
|
+
* ONE-WAY RULE. If `.ksor/people.yaml` maps this exact identifier to a name, we
|
|
11
|
+
* print `Human: Bashir Aziz`. If it does not, we print the identifier unchanged
|
|
12
|
+
* (`human:bashiraziz`, `ksor-starter/0.0.47`). No convention-based splitting,
|
|
13
|
+
* no camelCase / kebab-case guessing, and no derivation of a handle from a
|
|
14
|
+
* name: an owner is the only source of a display name.
|
|
15
|
+
*
|
|
16
|
+
* A producer actor (`<producer>/<version>`) has no `<kind>:` prefix and no
|
|
17
|
+
* natural name to look up, so it passes through unchanged — which is correct:
|
|
18
|
+
* a tool that approved a document should be named plainly, not humanised.
|
|
19
|
+
*
|
|
20
|
+
* NO IMPORTS: a leaf, like `lifecycle-rule.ts`. The phone book is handed in by
|
|
21
|
+
* the caller rather than reached for, so the rule can be exercised without a
|
|
22
|
+
* record on disk — and so nothing pulls a filesystem read in behind it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** The prefixes KSoR's actor grammar defines, in the case the site prints. */
|
|
26
|
+
const KIND_LABELS: Record<string, string> = {
|
|
27
|
+
human: "Human",
|
|
28
|
+
process: "Process",
|
|
29
|
+
team: "Team",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The display form of an actor. An actor that does not appear in
|
|
34
|
+
* `.ksor/people.yaml` renders exactly as stored, so nothing regresses on a
|
|
35
|
+
* record that has declared no names — which is every record until an owner
|
|
36
|
+
* says otherwise.
|
|
37
|
+
*/
|
|
38
|
+
export function displayActor(actor: string, people: ReadonlyMap<string, string>): string {
|
|
39
|
+
const colonAt = actor.indexOf(":");
|
|
40
|
+
if (colonAt === -1) {
|
|
41
|
+
// No `<kind>:` prefix — this is a producer like `ksor-starter/0.0.47`.
|
|
42
|
+
// Print it unchanged: humanising a tool's identifier would misread.
|
|
43
|
+
return actor;
|
|
44
|
+
}
|
|
45
|
+
// Looked up by the WHOLE identifier, not the bare handle: `human:ops` and
|
|
46
|
+
// `team:ops` are different actors, and a phone book keyed on `ops` would
|
|
47
|
+
// print one of them under the other's name.
|
|
48
|
+
const name = people.get(actor) ?? null;
|
|
49
|
+
if (name === null) return actor;
|
|
50
|
+
const kind = actor.slice(0, colonAt);
|
|
51
|
+
const kindLabel = KIND_LABELS[kind] ?? kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
52
|
+
return `${kindLabel}: ${name}`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Natural names for the actors this record cites — the site's phone book.
|
|
3
|
+
*
|
|
4
|
+
* A MAP keyed by the actor as stored (`human:bashiraziz`, `team:legal-ops`),
|
|
5
|
+
* not a list of names a handle is derived from. The derivation was the defect:
|
|
6
|
+
* `name.replace(/\s+/g, "").toLowerCase()` can only ever match a handle that
|
|
7
|
+
* IS somebody's squashed full name, so `human:ciso`, `human:audit-lead` and
|
|
8
|
+
* `human:mjs` — most of the actors in a real record — had no expressible name
|
|
9
|
+
* at all. It also collided: "Bashir Aziz" and "Bashira Ziz" both derive
|
|
10
|
+
* `bashiraziz`, which would print one person's name on the other's governance
|
|
11
|
+
* act. A map has neither problem, and duplicate keys are refused by the parser
|
|
12
|
+
* rather than resolved by whichever came last.
|
|
13
|
+
*
|
|
14
|
+
* ONE-WAY. The identifier is what the record stores, cites and checks against
|
|
15
|
+
* the policy; this is only what a page prints. Nothing reads a name back into
|
|
16
|
+
* an actor, and no authority follows from appearing here — which is why this is
|
|
17
|
+
* a file of its own and not a block in `.ksor/governance.yaml`: that file is
|
|
18
|
+
* the root of authority, its key set is closed on purpose, and its digest is
|
|
19
|
+
* hashed into `build.lock.json`, so correcting the spelling of someone's name
|
|
20
|
+
* there would refuse the next site build as `ksor-lock-stale`.
|
|
21
|
+
*
|
|
22
|
+
* Read from the project root rather than the process's cwd: `next build` runs
|
|
23
|
+
* in `system/site`, so a cwd-relative path found nothing and the feature was
|
|
24
|
+
* inert in exactly the builds that publish.
|
|
25
|
+
*
|
|
26
|
+
* Read AT USE and memoised, not at module load. A module-load `readFileSync`
|
|
27
|
+
* makes importing this module a filesystem act — it runs wherever the module is
|
|
28
|
+
* pulled in, including from a test that wants nothing but the display rule, and
|
|
29
|
+
* it fixes the answer before anything has had a chance to say where the record
|
|
30
|
+
* is. That is the same defect the env-tuning knobs had (#149/#194), one file
|
|
31
|
+
* over.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readFileSync } from "node:fs";
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
|
|
37
|
+
import { parseAllDocuments } from "yaml";
|
|
38
|
+
|
|
39
|
+
import { projectRoot } from "./shared";
|
|
40
|
+
|
|
41
|
+
const PEOPLE_YAML = path.join(projectRoot, ".ksor", "people.yaml");
|
|
42
|
+
|
|
43
|
+
function loadPeople(): ReadonlyMap<string, string> {
|
|
44
|
+
let text: string;
|
|
45
|
+
try {
|
|
46
|
+
text = readFileSync(PEOPLE_YAML, "utf8");
|
|
47
|
+
} catch {
|
|
48
|
+
// Optional: its absence means "no natural names declared".
|
|
49
|
+
return new Map();
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const docs = parseAllDocuments(text.replace(/^/, ""), {
|
|
53
|
+
schema: "core",
|
|
54
|
+
uniqueKeys: true,
|
|
55
|
+
logLevel: "silent",
|
|
56
|
+
});
|
|
57
|
+
const value: unknown = docs[0]?.toJS();
|
|
58
|
+
if (typeof value !== "object" || value === null) return new Map();
|
|
59
|
+
const table = (value as { people?: unknown }).people;
|
|
60
|
+
if (typeof table !== "object" || table === null || Array.isArray(table)) return new Map();
|
|
61
|
+
const out = new Map<string, string>();
|
|
62
|
+
for (const [actor, name] of Object.entries(table as Record<string, unknown>)) {
|
|
63
|
+
// A blank value is an entry someone started and left; printing "" would
|
|
64
|
+
// erase the identifier rather than replace it.
|
|
65
|
+
if (typeof name === "string" && name.trim() !== "") out.set(actor.trim(), name.trim());
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
} catch {
|
|
69
|
+
return new Map();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let cached: ReadonlyMap<string, string> | null = null;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What this record has declared. Memoised per process: the file is authored,
|
|
77
|
+
* not runtime state, and a static build renders many pages from one read.
|
|
78
|
+
*/
|
|
79
|
+
export function peopleBook(): ReadonlyMap<string, string> {
|
|
80
|
+
cached ??= loadPeople();
|
|
81
|
+
return cached;
|
|
82
|
+
}
|