@konneal/engine 0.2.4 → 0.2.6
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/README.md +2 -0
- package/dist/{ask-YCDXMIZ4.js → ask-RX3W5JJW.js} +2 -2
- package/dist/{chunk-OGFH3RDM.js → chunk-7K5XP4PB.js} +1 -1
- package/dist/{chunk-RLT4W2VX.js → chunk-GCL73ZLT.js} +1 -0
- package/dist/{chunk-THSHLUOS.js → chunk-NRC4FBDQ.js} +1 -1
- package/dist/{search-7TO2RWQE.js → search-OXHXXT5S.js} +2 -2
- package/dist/worker_public/src/index.js +41 -4
- package/docs/ADOPTION.md +61 -0
- package/package.json +1 -1
- package/workers/worker_internal/src/index.ts +29 -0
- package/workers/worker_public/src/index.ts +45 -0
- package/workers/worker_public/src/pipeline.ts +5 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Konneal Engine
|
|
2
2
|
|
|
3
|
+
**New here?** Read [docs/ADOPTION.md](docs/ADOPTION.md) — adopt Konneal for your own standards and your own identity service.
|
|
4
|
+
|
|
3
5
|
The publisher-agnostic serving layer for standards intelligence: hybrid
|
|
4
6
|
retrieval over clause-structured corpora, citation-grounded answers,
|
|
5
7
|
typed tables/formulas/figures, conformance checking by execution, and
|
|
@@ -1939,6 +1939,7 @@ ${context}` }
|
|
|
1939
1939
|
};
|
|
1940
1940
|
}
|
|
1941
1941
|
function publicationUrl(meta) {
|
|
1942
|
+
if (meta.corpus && !["oiml", "dirty", "clean"].includes(meta.corpus)) return void 0;
|
|
1942
1943
|
const tpl = P().publisher.catalog_url_template;
|
|
1943
1944
|
if (!tpl || !meta.doctype || !meta.doc_number) return void 0;
|
|
1944
1945
|
return tpl.replace("{type}", meta.doctype.toLowerCase()) + meta.doc_number;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleSearch
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-NRC4FBDQ.js";
|
|
4
4
|
import {
|
|
5
5
|
checkQuoteAnchors,
|
|
6
6
|
handleAsk,
|
|
7
7
|
handleMemories,
|
|
8
8
|
scoreJudge,
|
|
9
9
|
standardForDocNumber
|
|
10
|
-
} from "../../chunk-
|
|
10
|
+
} from "../../chunk-7K5XP4PB.js";
|
|
11
11
|
import {
|
|
12
12
|
buildMessages,
|
|
13
13
|
citations,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
sessionFrom,
|
|
30
30
|
telemetry,
|
|
31
31
|
understandQuery
|
|
32
|
-
} from "../../chunk-
|
|
32
|
+
} from "../../chunk-GCL73ZLT.js";
|
|
33
33
|
import {
|
|
34
34
|
authenticate,
|
|
35
35
|
corsHeaders,
|
|
@@ -979,7 +979,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
979
979
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
980
980
|
body: JSON.stringify({ ...args, stream: false })
|
|
981
981
|
});
|
|
982
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
982
|
+
const res = name === "ask" ? await (await import("../../ask-RX3W5JJW.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-OXHXXT5S.js")).handleSearch(env, ctx, inner, tier, key);
|
|
983
983
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
984
984
|
});
|
|
985
985
|
if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
|
|
@@ -1585,9 +1585,46 @@ async function researchRoute(c) {
|
|
|
1585
1585
|
const session = c.env.SESSION_SECRET ? await sessionFrom(c.req, c.env) : null;
|
|
1586
1586
|
return handleResearch(c.env, c.ctx, c.req, session);
|
|
1587
1587
|
}
|
|
1588
|
+
function permissionsCatalog() {
|
|
1589
|
+
const groups = [];
|
|
1590
|
+
const perms = [];
|
|
1591
|
+
for (const d of P().datasets ?? []) {
|
|
1592
|
+
if (d.session && d.permission) {
|
|
1593
|
+
perms.push({
|
|
1594
|
+
id: "ai.dataset.externally-licensed",
|
|
1595
|
+
description: `Access the ${d.label} dataset (federated, externally licensed content)`
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (P().publisher.features?.drafts) {
|
|
1600
|
+
perms.push({ id: "ai.drafts", description: "Ask the assistant to prepare draft acts" });
|
|
1601
|
+
}
|
|
1602
|
+
perms.push({ id: "ai.memories", description: "Personalized memory files on the assistant" });
|
|
1603
|
+
perms.push({ id: "ai.research", description: "Deep-research multi-pass questions" });
|
|
1604
|
+
perms.push({ id: "ai.keys.admin", description: "Create and revoke API keys" });
|
|
1605
|
+
groups.push({ id: "ai", description: "The publications assistant's gated capabilities", permissions: perms });
|
|
1606
|
+
return {
|
|
1607
|
+
version: 1,
|
|
1608
|
+
verbs: ["read", "write", "admin"],
|
|
1609
|
+
groups
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
async function openapiCatalogRoute(c) {
|
|
1613
|
+
return json(
|
|
1614
|
+
{
|
|
1615
|
+
openapi: "3.1.0",
|
|
1616
|
+
info: { title: `${P().publisher.product_name} API`, version: c.env.INDEX_VERSION ?? "0" },
|
|
1617
|
+
paths: {},
|
|
1618
|
+
"x-oiml-permissions-catalog": permissionsCatalog()
|
|
1619
|
+
},
|
|
1620
|
+
200,
|
|
1621
|
+
corsHeaders(c.req)
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1588
1624
|
var INFRA_ROUTES = [
|
|
1589
1625
|
{ method: "GET", pattern: "/", handler: serveIndexPage },
|
|
1590
1626
|
{ method: "GET", pattern: "/api/", handler: serveIndexPage },
|
|
1627
|
+
{ method: "GET", pattern: "/api/openapi.json", handler: openapiCatalogRoute },
|
|
1591
1628
|
{ method: "GET", pattern: "/index.html", handler: serveIndexPage },
|
|
1592
1629
|
{ method: "GET", pattern: "/assets/*", handler: unitAssetRoute },
|
|
1593
1630
|
{ method: "GET", pattern: "/docs/*", handler: docsRoute }
|
package/docs/ADOPTION.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Adopting Konneal: your own standards-intelligence site
|
|
2
|
+
|
|
3
|
+
Konneal is a publisher-agnostic engine: clause-cited, verifiable answers
|
|
4
|
+
over your own Metanorma documents and Primmel models — deployed on your
|
|
5
|
+
own infrastructure, with licensed third-party content (IEC, ISO and
|
|
6
|
+
peers) gated behind your identity service.
|
|
7
|
+
|
|
8
|
+
## Start
|
|
9
|
+
|
|
10
|
+
npm create @konneal/publisher my-sdo
|
|
11
|
+
|
|
12
|
+
The scaffolder interviews you for your publisher's facts — id, name,
|
|
13
|
+
domains, identity issuer, first dataset — and writes the whole
|
|
14
|
+
deployment skeleton: the worker, the site, the profile, the ingest
|
|
15
|
+
pipeline, CI. Every generated file is ordinary reviewable code.
|
|
16
|
+
|
|
17
|
+
Then the four declarations:
|
|
18
|
+
|
|
19
|
+
1. **Your profile** (`profile/*.yaml`): publisher identity, domains,
|
|
20
|
+
datasets, the corpora registry, the licensed-standards catalog,
|
|
21
|
+
prompt variables, smoke probes. Everything the serving surface knows
|
|
22
|
+
about you is this data, never engine code.
|
|
23
|
+
2. **Your corpus** (`sources/`): your Metanorma documents. The ingest
|
|
24
|
+
pipeline chunks along the documents' own clause boundaries, embeds,
|
|
25
|
+
and upserts into your indexes.
|
|
26
|
+
3. **Your models** (Primmel packages): the machine-readable layer —
|
|
27
|
+
requirements, constraints with threshold expressions, severity
|
|
28
|
+
condition sets — so answers can carry server-computed verdicts, not
|
|
29
|
+
paraphrases.
|
|
30
|
+
4. **Your identity**: any discovery-compliant OIDC provider. Register
|
|
31
|
+
the site (authorization code + PKCE), map roles to datasets and
|
|
32
|
+
groups to licensed keys in the provider's configuration — revoking a
|
|
33
|
+
role removes access on the next request, no deploy.
|
|
34
|
+
|
|
35
|
+
## Licensed content, gated
|
|
36
|
+
|
|
37
|
+
Licensed standards are fail-closed by construction: each licensed
|
|
38
|
+
package carries a license key; the deployment's profile declares the
|
|
39
|
+
whitelist; a request without an entitled scope never sees the licensed
|
|
40
|
+
text — it is filtered before the model reads anything. An anonymous
|
|
41
|
+
request cannot reach internal indexes at all: the public surface holds
|
|
42
|
+
no binding to them, and the internal worker re-verifies every session
|
|
43
|
+
itself.
|
|
44
|
+
|
|
45
|
+
## The proof
|
|
46
|
+
|
|
47
|
+
Your golden suite gates every promotion: content witnesses, refusal
|
|
48
|
+
legs, licensed pairs in both directions (entitled content serves;
|
|
49
|
+
unentitled never appears), context-utilization and verdict legs. The
|
|
50
|
+
nightly runs unattended against production and fails loudly on drift.
|
|
51
|
+
|
|
52
|
+
## Ask it from your terminal
|
|
53
|
+
|
|
54
|
+
The konneal CLI ships with the client package: npm i -g @konneal/client, then KONNEAL_BASE and KONNEAL_KEY in the environment give you konneal ask, konneal search and konneal keys list — streaming answers with citations and verdict blocks, the same API the site uses behind the same tiering.
|
|
55
|
+
|
|
56
|
+
## The path
|
|
57
|
+
|
|
58
|
+
`npm create @konneal/publisher` → declare the profile → ingest your
|
|
59
|
+
corpus → author one model package and one golden leg → map identity →
|
|
60
|
+
promote through the gate. Adoption is done when your team runs the
|
|
61
|
+
nightly and reads the gate without help.
|
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@oimlsmart/oiml-pubid": "^1.2.1"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {},
|
|
37
|
-
"version": "0.2.
|
|
37
|
+
"version": "0.2.6",
|
|
38
38
|
"description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
|
|
39
39
|
"license": "BSD-3-Clause",
|
|
40
40
|
"type": "module",
|
|
@@ -17,6 +17,7 @@ export interface Env {
|
|
|
17
17
|
AI: any;
|
|
18
18
|
PUBLIC: any; // idx_oiml_public_v2
|
|
19
19
|
INTERNAL: any; // idx_iso_internal
|
|
20
|
+
DOCS: any; // rag-internal-docs — the internal renderings
|
|
20
21
|
CACHE: KVNamespace;
|
|
21
22
|
SESSION_SECRET: string;
|
|
22
23
|
INDEX_VERSION: string;
|
|
@@ -137,10 +138,38 @@ async function retrieveRoute(c: RouteContext): Promise<Response> {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
// The internal corpus's document door: rendered publications and their
|
|
142
|
+
// clause-anchor maps, session-gated like retrieval (the admin token also
|
|
143
|
+
// opens it for ops verification). Only docs/<name> shapes are served —
|
|
144
|
+
// html and the anchors map, nothing else.
|
|
145
|
+
async function docRoute(c: RouteContext): Promise<Response> {
|
|
146
|
+
const { env, req } = c;
|
|
147
|
+
const file = c.params.file ?? "";
|
|
148
|
+
if (!/^docs\/[a-z0-9-]+\.(html|anchors\.json)$/.test(`docs/${file}`)) {
|
|
149
|
+
return err(400, "invalid_input", "docs/<slug>.html or docs/<slug>.anchors.json");
|
|
150
|
+
}
|
|
151
|
+
const session = await sessionFrom(req, env as any);
|
|
152
|
+
const admin = env.ADMIN_TOKEN && req.headers.get("x-admin-token") === env.ADMIN_TOKEN;
|
|
153
|
+
if (!session && !admin) return err(401, "unauthorized", "Sign in required — internal corpus documents are member content.");
|
|
154
|
+
try {
|
|
155
|
+
const obj = await env.DOCS.get(`docs/${file}`);
|
|
156
|
+
if (!obj) return err(404, "not_found", "No rendered document at this address.");
|
|
157
|
+
return new Response(obj.body, {
|
|
158
|
+
headers: {
|
|
159
|
+
"content-type": file.endsWith(".json") ? "application/json" : "text/html; charset=utf-8",
|
|
160
|
+
"cache-control": "private, max-age=86400",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
return err(503, "docs_unavailable", "The document store is briefly busy.");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
140
168
|
// the same declarative route table the public worker dispatches through
|
|
141
169
|
// (TODO.impl/31) — one HTTP idiom across workers.
|
|
142
170
|
const ROUTES: Route[] = [
|
|
143
171
|
{ method: "GET", pattern: "/health", handler: healthRoute },
|
|
172
|
+
{ method: "GET", pattern: "/docs/:file", handler: docRoute },
|
|
144
173
|
{ method: "POST", pattern: "/admin/sync", handler: adminSyncRoute },
|
|
145
174
|
{ method: "POST", pattern: "/retrieve", handler: retrieveRoute },
|
|
146
175
|
{ method: "POST", pattern: "/api/retrieve", handler: retrieveRoute },
|
|
@@ -398,9 +398,54 @@ async function researchRoute(c: RouteContext): Promise<Response> {
|
|
|
398
398
|
// The non-API routes: the HTML pages, the unit-keyed figure assets and
|
|
399
399
|
// the rendered publication documents. Everything else is generated from
|
|
400
400
|
// the OpenAPI document.
|
|
401
|
+
|
|
402
|
+
// The service's own permission catalog (TODO.openapi/03): the identity
|
|
403
|
+
// service FETCHES this endpoint to verify every permission grant for
|
|
404
|
+
// this client — it never holds a copy. The catalog derives from the
|
|
405
|
+
// profile's own declarations (gated datasets, feature flags), so a
|
|
406
|
+
// deployment's catalog cannot drift from what it enforces.
|
|
407
|
+
function permissionsCatalog(): Record<string, unknown> {
|
|
408
|
+
const groups: Record<string, unknown>[] = [];
|
|
409
|
+
const perms: Record<string, unknown>[] = [];
|
|
410
|
+
for (const d of P().datasets ?? []) {
|
|
411
|
+
if (d.session && d.permission) {
|
|
412
|
+
perms.push({
|
|
413
|
+
id: "ai.dataset.externally-licensed",
|
|
414
|
+
description: `Access the ${d.label} dataset (federated, externally licensed content)`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (P().publisher.features?.drafts) {
|
|
419
|
+
perms.push({ id: "ai.drafts", description: "Ask the assistant to prepare draft acts" });
|
|
420
|
+
}
|
|
421
|
+
perms.push({ id: "ai.memories", description: "Personalized memory files on the assistant" });
|
|
422
|
+
perms.push({ id: "ai.research", description: "Deep-research multi-pass questions" });
|
|
423
|
+
perms.push({ id: "ai.keys.admin", description: "Create and revoke API keys" });
|
|
424
|
+
groups.push({ id: "ai", description: "The publications assistant's gated capabilities", permissions: perms });
|
|
425
|
+
return {
|
|
426
|
+
version: 1,
|
|
427
|
+
verbs: ["read", "write", "admin"],
|
|
428
|
+
groups,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function openapiCatalogRoute(c: RouteContext): Promise<Response> {
|
|
433
|
+
return json(
|
|
434
|
+
{
|
|
435
|
+
openapi: "3.1.0",
|
|
436
|
+
info: { title: `${P().publisher.product_name} API`, version: c.env.INDEX_VERSION ?? "0" },
|
|
437
|
+
paths: {},
|
|
438
|
+
"x-oiml-permissions-catalog": permissionsCatalog(),
|
|
439
|
+
},
|
|
440
|
+
200,
|
|
441
|
+
corsHeaders(c.req),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
401
445
|
const INFRA_ROUTES: Route[] = [
|
|
402
446
|
{ method: "GET", pattern: "/", handler: serveIndexPage },
|
|
403
447
|
{ method: "GET", pattern: "/api/", handler: serveIndexPage },
|
|
448
|
+
{ method: "GET", pattern: "/api/openapi.json", handler: openapiCatalogRoute },
|
|
404
449
|
{ method: "GET", pattern: "/index.html", handler: serveIndexPage },
|
|
405
450
|
{ method: "GET", pattern: "/assets/*", handler: unitAssetRoute },
|
|
406
451
|
{ method: "GET", pattern: "/docs/*", handler: docsRoute },
|
|
@@ -380,6 +380,11 @@ export function buildMessages(
|
|
|
380
380
|
* URL template ({type} = lowercase doctype, then the number); no
|
|
381
381
|
* template, no catalog link. */
|
|
382
382
|
function publicationUrl(meta: ChunkMeta): string | undefined {
|
|
383
|
+
// the catalog template names THIS publisher's store - an internal or
|
|
384
|
+
// foreign-corpus chunk has no entry in it, and a fabricated url is
|
|
385
|
+
// worse than none (the door for internal renderings is the internal
|
|
386
|
+
// origin, wired by the site's doc-base configuration)
|
|
387
|
+
if (meta.corpus && !["oiml", "dirty", "clean"].includes(meta.corpus)) return undefined;
|
|
383
388
|
const tpl = P().publisher.catalog_url_template;
|
|
384
389
|
if (!tpl || !meta.doctype || !meta.doc_number) return undefined;
|
|
385
390
|
return tpl.replace("{type}", meta.doctype.toLowerCase()) + meta.doc_number;
|