@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 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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-OGFH3RDM.js";
4
- import "./chunk-RLT4W2VX.js";
3
+ } from "./chunk-7K5XP4PB.js";
4
+ import "./chunk-GCL73ZLT.js";
5
5
  import "./chunk-EFQALN2Z.js";
6
6
  import "./chunk-DBBGOOMZ.js";
7
7
  import "./chunk-5MBWE7WD.js";
@@ -27,7 +27,7 @@ import {
27
27
  syntheticUnderstanding,
28
28
  telemetry,
29
29
  understandQuery
30
- } from "./chunk-RLT4W2VX.js";
30
+ } from "./chunk-GCL73ZLT.js";
31
31
  import {
32
32
  corsHeaders,
33
33
  err,
@@ -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;
@@ -7,7 +7,7 @@ import {
7
7
  sessionFrom,
8
8
  telemetry,
9
9
  understandQuery
10
- } from "./chunk-RLT4W2VX.js";
10
+ } from "./chunk-GCL73ZLT.js";
11
11
  import {
12
12
  corsHeaders,
13
13
  err,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleSearch
3
- } from "./chunk-THSHLUOS.js";
4
- import "./chunk-RLT4W2VX.js";
3
+ } from "./chunk-NRC4FBDQ.js";
4
+ import "./chunk-GCL73ZLT.js";
5
5
  import "./chunk-EFQALN2Z.js";
6
6
  import "./chunk-DBBGOOMZ.js";
7
7
  import "./chunk-5MBWE7WD.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  handleSearch
3
- } from "../../chunk-THSHLUOS.js";
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-OGFH3RDM.js";
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-RLT4W2VX.js";
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-YCDXMIZ4.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-7TO2RWQE.js")).handleSearch(env, ctx, inner, tier, key);
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 }
@@ -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.4",
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;