@pegma/knowledge-base-application 0.0.0-stage → 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 +140 -2
- package/dist/articles.d.ts +10 -0
- package/dist/articles.d.ts.map +1 -0
- package/dist/articles.js +454 -0
- package/dist/articles.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/governance.d.ts +14 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/governance.js +364 -0
- package/dist/governance.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/lexical.d.ts +13 -0
- package/dist/lexical.d.ts.map +1 -0
- package/dist/lexical.js +105 -0
- package/dist/lexical.js.map +1 -0
- package/dist/publication.d.ts +42 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +258 -0
- package/dist/publication.js.map +1 -0
- package/dist/reads.d.ts +5 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +228 -0
- package/dist/reads.js.map +1 -0
- package/dist/records.d.ts +40 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +122 -0
- package/dist/records.js.map +1 -0
- package/dist/repository.d.ts +74 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +738 -0
- package/dist/repository.js.map +1 -0
- package/dist/retrieval-budget.d.ts +37 -0
- package/dist/retrieval-budget.d.ts.map +1 -0
- package/dist/retrieval-budget.js +111 -0
- package/dist/retrieval-budget.js.map +1 -0
- package/dist/retrieval-limits.d.ts +4 -0
- package/dist/retrieval-limits.d.ts.map +1 -0
- package/dist/retrieval-limits.js +21 -0
- package/dist/retrieval-limits.js.map +1 -0
- package/dist/retrieval-maintenance.d.ts +19 -0
- package/dist/retrieval-maintenance.d.ts.map +1 -0
- package/dist/retrieval-maintenance.js +359 -0
- package/dist/retrieval-maintenance.js.map +1 -0
- package/dist/retrieval-records.d.ts +55 -0
- package/dist/retrieval-records.d.ts.map +1 -0
- package/dist/retrieval-records.js +140 -0
- package/dist/retrieval-records.js.map +1 -0
- package/dist/retrieval-repository.d.ts +32 -0
- package/dist/retrieval-repository.d.ts.map +1 -0
- package/dist/retrieval-repository.js +750 -0
- package/dist/retrieval-repository.js.map +1 -0
- package/dist/retrieval-types.d.ts +185 -0
- package/dist/retrieval-types.d.ts.map +1 -0
- package/dist/retrieval-types.js +24 -0
- package/dist/retrieval-types.js.map +1 -0
- package/dist/retrieval.d.ts +12 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +541 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/storage-helpers.d.ts +23 -0
- package/dist/storage-helpers.d.ts.map +1 -0
- package/dist/storage-helpers.js +138 -0
- package/dist/storage-helpers.js.map +1 -0
- package/dist/types.d.ts +280 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pegma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,141 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pegma/knowledge-base-application
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This unpublished KB-02/KB-03 source package composes contracts/core with
|
|
4
|
+
host-supplied Storage Core, Authorization Core, Audit, and a clock. Source
|
|
5
|
+
implementation and repository gates determine task completion; neither this
|
|
6
|
+
package nor local tests establish a live host or production readiness.
|
|
7
|
+
|
|
8
|
+
The [declared layout](https://github.com/pegma-dev/knowledge-base/blob/main/docs/COLLECTIONS.md) records the agreement made
|
|
9
|
+
before durable operations. [Host composition](https://github.com/pegma-dev/knowledge-base/blob/main/docs/HOST_COMPOSITION.md) describes the host
|
|
10
|
+
authentication, current-resource authorization, verified-evidence, and privacy
|
|
11
|
+
boundaries. [Retrieval](https://github.com/pegma-dev/knowledge-base/blob/main/docs/RETRIEVAL.md)
|
|
12
|
+
documents bounded lexical lookup, authoritative confirmation, citations, and
|
|
13
|
+
repair. The optional `@pegma/knowledge-base-mcp` package adds HTTP/MCP adapters
|
|
14
|
+
over this API; it supplies no login or token issuer. Real-host acceptance and
|
|
15
|
+
production activation remain separately gated.
|
|
16
|
+
|
|
17
|
+
GitHub links in this unpublished README track development `main`; they are
|
|
18
|
+
not frozen version documentation. KB-05 release preparation must pin those
|
|
19
|
+
references to verified release source before first publication.
|
|
20
|
+
|
|
21
|
+
The application API owns authorized commands, application-generation CAS,
|
|
22
|
+
bounded immutable revision/evidence history, exact review/publication,
|
|
23
|
+
transactional Audit/receipts/causal jobs, bounded retrieval/repair, and safe
|
|
24
|
+
published-content DTOs.
|
|
25
|
+
Collections own audience policy; articles inherit it without per-article ACLs.
|
|
26
|
+
Role models, token issuance, sessions, HTTP, provider SDKs, arbitrary URL
|
|
27
|
+
fetching, code execution, and a new database are outside this package.
|
|
28
|
+
|
|
29
|
+
Passing evidence requires a host-authenticated verifier receipt bound to the
|
|
30
|
+
exact submitted evidence. Echoing caller-supplied `passed`, runner identity,
|
|
31
|
+
or an attestation object does not establish proof. Command replay likewise
|
|
32
|
+
rechecks current permission and resource authority before returning a result.
|
|
33
|
+
Never expose raw aggregates or private evidence/source references to customers
|
|
34
|
+
or a support model.
|
|
35
|
+
|
|
36
|
+
## Composition and commands
|
|
37
|
+
|
|
38
|
+
`createKnowledgeBaseApplication` takes `store` and `clock`, with optional
|
|
39
|
+
`limits`, `evidenceVerifier`, `privateSources`, and nested `retrieval` settings.
|
|
40
|
+
Hosts authenticate callers
|
|
41
|
+
and resolve current scoped `AccessContext` values. The
|
|
42
|
+
[public types](https://github.com/pegma-dev/knowledge-base/blob/main/packages/application/src/types.ts) define commands and purpose-specific results;
|
|
43
|
+
the package owns no HTTP routes or credentials.
|
|
44
|
+
|
|
45
|
+
Creates use `expectedGeneration: 0`; later mutations use exact application
|
|
46
|
+
generation, which also advances for evidence/review/provenance changes.
|
|
47
|
+
Content revision advances only when content changes. All mutation inputs
|
|
48
|
+
include a stable `commandId` and observability `correlationId`. The service
|
|
49
|
+
derives actor/time from the trusted context and clock.
|
|
50
|
+
|
|
51
|
+
Collection/validation/governance commands establish private defaults, current
|
|
52
|
+
validation requirements, staging applicability, and trusted attesters. Article
|
|
53
|
+
commands create/update drafts, submit verified evidence and review, publish an
|
|
54
|
+
exact `ReviewSnapshot`, reject, retire, set private provenance, or revoke an
|
|
55
|
+
exact historical proof. The snapshot includes the core approval binding plus
|
|
56
|
+
article/source/governance generations.
|
|
57
|
+
|
|
58
|
+
`CommandResult` returns `{ value, replayed }`, where `value` is a bounded
|
|
59
|
+
historical acknowledgement. It contains no body/evidence/provenance. Replay
|
|
60
|
+
rechecks current operation/resource authority, conflicts on changed semantic
|
|
61
|
+
input, and does not claim the historical result is currently servable.
|
|
62
|
+
|
|
63
|
+
## Reads and trust
|
|
64
|
+
|
|
65
|
+
`getDraft`, `getEvidence`, `getProvenance`, and `getReview` have distinct
|
|
66
|
+
permissions and DTOs. Private references additionally require current source
|
|
67
|
+
resource authorization. `getValidationRequirements` gives authorized authors
|
|
68
|
+
current kind-specific checks, policy reference, and staging build without
|
|
69
|
+
disclosing trusted attester IDs. `getPublishedArticle` is a portal-safe keyed read:
|
|
70
|
+
it checks the published collection's current portal audience and returns only
|
|
71
|
+
approved content/safe metadata. It is not KB-03 search or bot-only retrieval.
|
|
72
|
+
|
|
73
|
+
`EvidenceVerifier.verify` authenticates an opaque receipt against the entire
|
|
74
|
+
submission, authenticated actor, exact article, and governance configuration.
|
|
75
|
+
It returns trusted attestation facts or null. A missing verifier fails closed.
|
|
76
|
+
`PrivateSourcesVerifier.canRead` controls private reference disclosure;
|
|
77
|
+
`canPublish` and `isCurrent` independently attest source eligibility without
|
|
78
|
+
making source tickets readable to customers. Missing required gates fail
|
|
79
|
+
closed for nonempty source snapshots.
|
|
80
|
+
|
|
81
|
+
An existing publication retains its own review/source snapshot and exact
|
|
82
|
+
historical proof references while a new draft is prepared. Replacing current
|
|
83
|
+
evidence does not revoke an old proof; explicit revocation holds an affected
|
|
84
|
+
publication and clears pending review. Current reads reject stale governance
|
|
85
|
+
or source authority. Retained evidence freshness is checked at the recorded
|
|
86
|
+
approval time; later aging alone does not expire an approved guide. New review
|
|
87
|
+
and publication use current time, while current policy/source eligibility,
|
|
88
|
+
proof availability, and revocation remain live checks. Article CAS cannot
|
|
89
|
+
globally fence separate collection/
|
|
90
|
+
validation-policy partitions.
|
|
91
|
+
|
|
92
|
+
## Bounded retrieval and repair
|
|
93
|
+
|
|
94
|
+
`listPortalCollections` and `searchPublishedArticles` require current portal
|
|
95
|
+
authority. `retrieveAnswerSources` and `confirmAnswerSources` use the effective
|
|
96
|
+
customer's `articleRead`, `botRead`, independent bot audience, and exact product
|
|
97
|
+
applicability. Read results report `results` or `no_result`, never candidate
|
|
98
|
+
totals. Titles, snippets, content, and citations come from freshly confirmed
|
|
99
|
+
publications rather than index text. Bot-only citations have a safe label;
|
|
100
|
+
`portalArticle` appears only when that customer can also browse the article.
|
|
101
|
+
|
|
102
|
+
The default lexical projection uses approved titles, summaries, and tags, not
|
|
103
|
+
body text. Requests use only bounded keyed reads. Optional
|
|
104
|
+
`retrieval.candidateAdapter` hints remain untrusted; the host adapter must
|
|
105
|
+
report all physical work, including empty pages and discarded rows.
|
|
106
|
+
Adapters may match approved body text/synonyms with zero metadata overlap;
|
|
107
|
+
bounded preselection preserves their order, and confirmed metadata scores
|
|
108
|
+
rank results with adapter-order ties. Hosts own relevance evaluation and
|
|
109
|
+
provider data controls; current publication checks remain mandatory.
|
|
110
|
+
`retrieval.tokenCounter` is a trusted local counter over the complete
|
|
111
|
+
`serializedSources` returned with `sourceTokens`. Retrieval fits the largest
|
|
112
|
+
ranked prefix of whole articles within aggregate byte/token budgets with a
|
|
113
|
+
positive local count; reconfirmation is exact and all-or-nothing.
|
|
114
|
+
`retrieval.limits` can only lower fixed component ceilings.
|
|
115
|
+
See [retrieval](https://github.com/pegma-dev/knowledge-base/blob/main/docs/RETRIEVAL.md)
|
|
116
|
+
for the exact API, limits, authority/reference fences, and evaluation boundary.
|
|
117
|
+
|
|
118
|
+
`repairArticleIndex`, `repairRetrievalPage`, and `getArticleIndexStatus` require
|
|
119
|
+
`indexManage`. Only maintenance performs bounded physical scans with opaque
|
|
120
|
+
cursors. Pending/blocked status remains visible until repair converges;
|
|
121
|
+
`ready` with `indexed: false` means a converged withdrawal. Projection cleanup
|
|
122
|
+
may lag retirement/revocation, but current authority checks still reject
|
|
123
|
+
ineligible publications. The host owns scheduling and recovery.
|
|
124
|
+
|
|
125
|
+
## Storage and limits
|
|
126
|
+
|
|
127
|
+
All accepted head changes, immutable rows, generic Audit actions, receipts, and
|
|
128
|
+
causal jobs share one resource partition in `knowledge-base.records.v1`.
|
|
129
|
+
Native flat chunk properties keep bounded rows compatible with the host's
|
|
130
|
+
store adapter. The application separately bounds physical rows, encoded bytes,
|
|
131
|
+
history, transaction work, retries, and control reserve. Read the
|
|
132
|
+
[layout limits](https://github.com/pegma-dev/knowledge-base/blob/main/docs/COLLECTIONS.md#physical-representation-and-hard-bounds)
|
|
133
|
+
before changing admission or retention; these are component safety bounds,
|
|
134
|
+
not measured host throughput. No private persistence or parallel audit log is
|
|
135
|
+
implemented here.
|
|
136
|
+
|
|
137
|
+
Merging source does not publish a package. Hosts must wait for the exact public
|
|
138
|
+
release and normal release/host activation gates. See
|
|
139
|
+
[release operations](https://github.com/pegma-dev/knowledge-base/blob/main/docs/RELEASING.md).
|
|
140
|
+
|
|
141
|
+
Built in the open by RetireGolden, LLC.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { KnowledgeRepository } from "./repository.js";
|
|
2
|
+
import type { KnowledgeBaseApplication, KnowledgeBaseServiceOptions } from "./types.js";
|
|
3
|
+
type ArticleMethods = Pick<KnowledgeBaseApplication, "createDraft" | "updateDraft" | "submitEvidence" | "submitForReview" | "publishArticle" | "rejectReview" | "retireArticle" | "setProvenance" | "revokeEvidence">;
|
|
4
|
+
/**
|
|
5
|
+
* Instance-wide staff authoring permissions are explicit. Portal/bot audience
|
|
6
|
+
* authorization is applied by the separate safe-read services.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createArticleServices(repository: KnowledgeRepository, options: KnowledgeBaseServiceOptions): ArticleMethods;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=articles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["../src/articles.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EACV,mBAAmB,EAGpB,MAAM,iBAAiB,CAAC;AAWzB,OAAO,KAAK,EAIV,wBAAwB,EACxB,2BAA2B,EAI5B,MAAM,YAAY,CAAC;AAEpB,KAAK,cAAc,GAAG,IAAI,CACxB,wBAAwB,EACtB,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,gBAAgB,GAChB,cAAc,GACd,eAAe,GACf,eAAe,GACf,gBAAgB,CACnB,CAAC;AAoLF;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,mBAAmB,EAC/B,OAAO,EAAE,2BAA2B,GACnC,cAAc,CAogBhB"}
|
package/dist/articles.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { knowledgePermissions, } from "@pegma/knowledge-base-contracts";
|
|
2
|
+
import { createArticle, KnowledgeBaseError, publishArticle as publishCoreArticle, recordEvidence, rejectReview as rejectCoreReview, retireArticle as retireCoreArticle, reviseArticle, submitForReview as submitCoreReview, } from "@pegma/knowledge-base-core";
|
|
3
|
+
import { KnowledgeBaseNotFoundError, KnowledgeBasePrerequisiteError, requirePermission, } from "./errors.js";
|
|
4
|
+
import { loadGovernanceContext } from "./governance.js";
|
|
5
|
+
import { maximumRecordNativeBytes } from "./records.js";
|
|
6
|
+
import { canonicalJson, fingerprint, RepositoryCapacityError, RepositoryConflictError, RepositoryIntegrityError, requireGeneration, requireIdentifier, snapshotData, } from "./storage-helpers.js";
|
|
7
|
+
const commandFields = [
|
|
8
|
+
"commandId",
|
|
9
|
+
"correlationId",
|
|
10
|
+
"expectedGeneration",
|
|
11
|
+
"articleId",
|
|
12
|
+
];
|
|
13
|
+
const maximumSourceReferences = 32;
|
|
14
|
+
function boundedText(value, field, maximum = 1_000) {
|
|
15
|
+
if (typeof value !== "string" ||
|
|
16
|
+
value.length === 0 ||
|
|
17
|
+
value.length > maximum ||
|
|
18
|
+
value.trim() !== value ||
|
|
19
|
+
/[\u0000-\u001f\u007f]/u.test(value))
|
|
20
|
+
throw new TypeError(field + " must be bounded nonempty text");
|
|
21
|
+
}
|
|
22
|
+
function allowedFields(value, allowed, field) {
|
|
23
|
+
if (value === null ||
|
|
24
|
+
typeof value !== "object" ||
|
|
25
|
+
Array.isArray(value) ||
|
|
26
|
+
Object.keys(value).some((key) => !allowed.includes(key)))
|
|
27
|
+
throw new TypeError(field + " contains unsupported fields");
|
|
28
|
+
}
|
|
29
|
+
function commandSnapshot(access, input, permission, extra) {
|
|
30
|
+
const caller = snapshotData(access);
|
|
31
|
+
requirePermission(caller, permission);
|
|
32
|
+
const encoded = canonicalJson(input);
|
|
33
|
+
if (new TextEncoder().encode(encoded).byteLength > maximumRecordNativeBytes) {
|
|
34
|
+
throw new RepositoryCapacityError("command_bytes");
|
|
35
|
+
}
|
|
36
|
+
const command = JSON.parse(encoded);
|
|
37
|
+
allowedFields(command, [...commandFields, ...extra], "command");
|
|
38
|
+
requireIdentifier(caller.principalId, "principalId");
|
|
39
|
+
requireIdentifier(command.articleId, "articleId");
|
|
40
|
+
requireIdentifier(command.commandId, "commandId");
|
|
41
|
+
requireIdentifier(command.correlationId, "correlationId");
|
|
42
|
+
requireGeneration(command.expectedGeneration);
|
|
43
|
+
return { access: caller, input: command };
|
|
44
|
+
}
|
|
45
|
+
function requireArticle(current) {
|
|
46
|
+
if (current === null)
|
|
47
|
+
throw new KnowledgeBaseNotFoundError("article");
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
function nextState(current) {
|
|
51
|
+
const { generation: _generation, ...next } = current;
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
function withoutPendingReview(current) {
|
|
55
|
+
const { review: _review, ...next } = nextState(current);
|
|
56
|
+
return {
|
|
57
|
+
...next,
|
|
58
|
+
article: current.article.state === "in-review"
|
|
59
|
+
? rejectCoreReview(current.article, current.article.current.number)
|
|
60
|
+
: current.article,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function acknowledgement(result) {
|
|
64
|
+
return {
|
|
65
|
+
value: {
|
|
66
|
+
id: result.value.article.id,
|
|
67
|
+
generation: result.value.generation,
|
|
68
|
+
articleRevision: result.value.article.current.number,
|
|
69
|
+
state: result.value.article.state,
|
|
70
|
+
contentHash: result.value.article.current.contentHash,
|
|
71
|
+
},
|
|
72
|
+
replayed: result.replayed,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function validateReferences(references) {
|
|
76
|
+
if (!Array.isArray(references) ||
|
|
77
|
+
references.length > maximumSourceReferences) {
|
|
78
|
+
throw new RepositoryCapacityError("source_references");
|
|
79
|
+
}
|
|
80
|
+
const ids = new Set();
|
|
81
|
+
for (const reference of references) {
|
|
82
|
+
allowedFields(reference, ["id", "system", "resourceId", "version"], "source reference");
|
|
83
|
+
for (const [field, value] of Object.entries(reference)) {
|
|
84
|
+
boundedText(value, "source." + field, 200);
|
|
85
|
+
if (value.includes("://"))
|
|
86
|
+
throw new TypeError("Source references must use opaque identifiers");
|
|
87
|
+
}
|
|
88
|
+
for (const field of ["id", "system", "resourceId", "version"]) {
|
|
89
|
+
requireIdentifier(reference[field], "source." + field);
|
|
90
|
+
}
|
|
91
|
+
if (ids.has(reference.id))
|
|
92
|
+
throw new TypeError("Source reference ids must be unique");
|
|
93
|
+
ids.add(reference.id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function sourceSnapshot(generation, references) {
|
|
97
|
+
requireGeneration(generation, 1);
|
|
98
|
+
validateReferences(references);
|
|
99
|
+
const fields = { generation, references: snapshotData(references) };
|
|
100
|
+
return { ...fields, hash: await fingerprint(fields) };
|
|
101
|
+
}
|
|
102
|
+
async function verifySourceSnapshot(snapshot) {
|
|
103
|
+
requireGeneration(snapshot.generation, 1);
|
|
104
|
+
validateReferences(snapshot.references);
|
|
105
|
+
if (snapshot.hash !==
|
|
106
|
+
(await fingerprint({
|
|
107
|
+
generation: snapshot.generation,
|
|
108
|
+
references: snapshot.references,
|
|
109
|
+
})))
|
|
110
|
+
throw new RepositoryIntegrityError();
|
|
111
|
+
}
|
|
112
|
+
function rejectRevokedCurrentEvidence(current) {
|
|
113
|
+
if (current.revokedEvidence.some((revocation) => revocation.articleRevision === current.article.current.number &&
|
|
114
|
+
current.article.evidence.some((evidence) => evidence.id === revocation.id &&
|
|
115
|
+
evidence.version === revocation.version &&
|
|
116
|
+
evidence.hash === revocation.hash))) {
|
|
117
|
+
throw new KnowledgeBaseError("evidence", "evidence", "Current evidence has been revoked");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Instance-wide staff authoring permissions are explicit. Portal/bot audience
|
|
122
|
+
* authorization is applied by the separate safe-read services.
|
|
123
|
+
*/
|
|
124
|
+
export function createArticleServices(repository, options) {
|
|
125
|
+
async function sourcesEligible(article, snapshot) {
|
|
126
|
+
await verifySourceSnapshot(snapshot);
|
|
127
|
+
if (snapshot.references.length === 0)
|
|
128
|
+
return;
|
|
129
|
+
const verifier = options.privateSources;
|
|
130
|
+
if (typeof verifier?.canPublish !== "function" ||
|
|
131
|
+
typeof verifier.isCurrent !== "function")
|
|
132
|
+
throw new KnowledgeBasePrerequisiteError("private-source-verifier");
|
|
133
|
+
try {
|
|
134
|
+
const request = {
|
|
135
|
+
article: snapshotData(article),
|
|
136
|
+
snapshot: snapshotData(snapshot),
|
|
137
|
+
};
|
|
138
|
+
if ((await verifier.isCurrent(snapshotData(request))) !== true ||
|
|
139
|
+
(await verifier.canPublish(snapshotData(request))) !== true) {
|
|
140
|
+
throw new KnowledgeBasePrerequisiteError("source-eligibility");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new KnowledgeBasePrerequisiteError("source-eligibility");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function requireSourceAccess(access, articleId, references) {
|
|
148
|
+
validateReferences(references);
|
|
149
|
+
if (references.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
const verifier = options.privateSources;
|
|
152
|
+
if (typeof verifier?.canRead !== "function") {
|
|
153
|
+
throw new KnowledgeBasePrerequisiteError("private-source-verifier");
|
|
154
|
+
}
|
|
155
|
+
for (const reference of references) {
|
|
156
|
+
try {
|
|
157
|
+
if ((await verifier.canRead({
|
|
158
|
+
access: snapshotData(access),
|
|
159
|
+
articleId,
|
|
160
|
+
reference: snapshotData(reference),
|
|
161
|
+
})) !== true)
|
|
162
|
+
throw new KnowledgeBasePrerequisiteError("source-access");
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new KnowledgeBasePrerequisiteError("source-access");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function execute(access, input, permission, operation, extraFields, decide, control = false, create = false) {
|
|
170
|
+
const request = commandSnapshot(access, input, permission, extraFields);
|
|
171
|
+
if (create
|
|
172
|
+
? request.input.expectedGeneration !== 0
|
|
173
|
+
: request.input.expectedGeneration < 1) {
|
|
174
|
+
throw new RepositoryConflictError();
|
|
175
|
+
}
|
|
176
|
+
const command = {
|
|
177
|
+
commandId: request.input.commandId,
|
|
178
|
+
correlationId: request.input.correlationId,
|
|
179
|
+
actorId: request.access.principalId,
|
|
180
|
+
operation,
|
|
181
|
+
fingerprint: await fingerprint({
|
|
182
|
+
actorId: request.access.principalId,
|
|
183
|
+
operation,
|
|
184
|
+
payload: Object.fromEntries(Object.entries(request.input).filter(([field]) => field !== "commandId" && field !== "correlationId")),
|
|
185
|
+
}),
|
|
186
|
+
occurredAt: options.clock.now(),
|
|
187
|
+
};
|
|
188
|
+
const result = await repository.executeArticle({
|
|
189
|
+
id: request.input.articleId,
|
|
190
|
+
expectedGeneration: request.input.expectedGeneration,
|
|
191
|
+
command,
|
|
192
|
+
control,
|
|
193
|
+
recheck: async (current) => {
|
|
194
|
+
requirePermission(request.access, permission);
|
|
195
|
+
if (!create)
|
|
196
|
+
requireArticle(current);
|
|
197
|
+
// Authoring is instance-wide, but an ordinary operation/replay still
|
|
198
|
+
// confirms its current collection exists. Safety withdrawals do not
|
|
199
|
+
// depend on unrelated collection/provider configuration being healthy.
|
|
200
|
+
if (current !== null && !control) {
|
|
201
|
+
const collection = await repository.readCollectionVersioned(current.article.current.collectionId);
|
|
202
|
+
if (collection === null)
|
|
203
|
+
throw new KnowledgeBaseNotFoundError("collection");
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
decide: async (current, generation) => decide(current, generation, request.input, request.access),
|
|
207
|
+
});
|
|
208
|
+
return acknowledgement(result);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
createDraft(access, input) {
|
|
212
|
+
return execute(access, input, knowledgePermissions.draftWrite, "article.create", ["collectionId", "draft"], async (current, _generation, command, caller) => {
|
|
213
|
+
if (current !== null)
|
|
214
|
+
throw new RepositoryConflictError();
|
|
215
|
+
requireIdentifier(command.collectionId, "collectionId");
|
|
216
|
+
const governance = await loadGovernanceContext(repository, command.collectionId, options.clock.now());
|
|
217
|
+
const article = await createArticle({
|
|
218
|
+
id: command.articleId,
|
|
219
|
+
draft: command.draft,
|
|
220
|
+
context: { ...governance.context, actorId: caller.principalId },
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
next: {
|
|
224
|
+
article,
|
|
225
|
+
provenance: await sourceSnapshot(1, []),
|
|
226
|
+
publicationHeld: false,
|
|
227
|
+
revokedEvidence: [],
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}, false, true);
|
|
231
|
+
},
|
|
232
|
+
updateDraft(access, input) {
|
|
233
|
+
return execute(access, input, knowledgePermissions.draftWrite, "article.update", ["collectionId", "draft"], async (current, _generation, command, caller) => {
|
|
234
|
+
const stored = requireArticle(current);
|
|
235
|
+
requireIdentifier(command.collectionId, "collectionId");
|
|
236
|
+
const governance = await loadGovernanceContext(repository, command.collectionId, options.clock.now());
|
|
237
|
+
const article = await reviseArticle(stored.article, {
|
|
238
|
+
expectedRevision: stored.article.current.number,
|
|
239
|
+
draft: command.draft,
|
|
240
|
+
context: { ...governance.context, actorId: caller.principalId },
|
|
241
|
+
});
|
|
242
|
+
return { next: { ...withoutPendingReview(stored), article } };
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
submitEvidence(access, input) {
|
|
246
|
+
return execute(access, input, knowledgePermissions.evidenceSubmit, "article.evidence.submit", ["evidence", "receipt"], async (current, _generation, command, caller) => {
|
|
247
|
+
const stored = requireArticle(current);
|
|
248
|
+
boundedText(command.receipt, "receipt", 8_192);
|
|
249
|
+
allowedFields(command.evidence, [
|
|
250
|
+
"id",
|
|
251
|
+
"articleId",
|
|
252
|
+
"articleRevision",
|
|
253
|
+
"contentHash",
|
|
254
|
+
"validationPolicy",
|
|
255
|
+
"checkId",
|
|
256
|
+
"testKind",
|
|
257
|
+
"applicability",
|
|
258
|
+
"stagingBuildId",
|
|
259
|
+
"steps",
|
|
260
|
+
"result",
|
|
261
|
+
"executedAt",
|
|
262
|
+
"runnerId",
|
|
263
|
+
"artifacts",
|
|
264
|
+
], "evidence submission");
|
|
265
|
+
const verifier = options.evidenceVerifier;
|
|
266
|
+
if (typeof verifier?.verify !== "function") {
|
|
267
|
+
throw new KnowledgeBasePrerequisiteError("evidence-verifier");
|
|
268
|
+
}
|
|
269
|
+
const before = await loadGovernanceContext(repository, stored.article.current.collectionId, options.clock.now());
|
|
270
|
+
const collection = await repository.readCollectionVersioned(stored.article.current.collectionId);
|
|
271
|
+
if (collection?.value.configuration === undefined) {
|
|
272
|
+
throw new KnowledgeBasePrerequisiteError("governance-configuration");
|
|
273
|
+
}
|
|
274
|
+
let attestation;
|
|
275
|
+
try {
|
|
276
|
+
const verified = await verifier.verify({
|
|
277
|
+
access: snapshotData(caller),
|
|
278
|
+
article: snapshotData(stored.article.current),
|
|
279
|
+
submission: snapshotData(command.evidence),
|
|
280
|
+
receipt: command.receipt,
|
|
281
|
+
configuration: snapshotData(collection.value.configuration),
|
|
282
|
+
});
|
|
283
|
+
attestation =
|
|
284
|
+
verified === null || verified === undefined
|
|
285
|
+
? null
|
|
286
|
+
: snapshotData(verified);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
throw new KnowledgeBasePrerequisiteError("evidence-verifier");
|
|
290
|
+
}
|
|
291
|
+
if (attestation === null || attestation === undefined) {
|
|
292
|
+
throw new KnowledgeBasePrerequisiteError("evidence-verifier");
|
|
293
|
+
}
|
|
294
|
+
const after = await loadGovernanceContext(repository, stored.article.current.collectionId, options.clock.now());
|
|
295
|
+
if (before.governanceGeneration !== after.governanceGeneration ||
|
|
296
|
+
before.validationGeneration !== after.validationGeneration ||
|
|
297
|
+
collection.value.generation !== before.governanceGeneration)
|
|
298
|
+
throw new RepositoryConflictError();
|
|
299
|
+
const article = await recordEvidence(stored.article, {
|
|
300
|
+
evidence: command.evidence,
|
|
301
|
+
attestation,
|
|
302
|
+
context: after.context,
|
|
303
|
+
});
|
|
304
|
+
return { next: { ...withoutPendingReview(stored), article } };
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
submitForReview(access, input) {
|
|
308
|
+
return execute(access, input, knowledgePermissions.reviewSubmit, "article.review.submit", [], async (current, generation) => {
|
|
309
|
+
const stored = requireArticle(current);
|
|
310
|
+
rejectRevokedCurrentEvidence(stored);
|
|
311
|
+
const before = await loadGovernanceContext(repository, stored.article.current.collectionId, options.clock.now());
|
|
312
|
+
await sourcesEligible(stored.article.current, stored.provenance);
|
|
313
|
+
const after = await loadGovernanceContext(repository, stored.article.current.collectionId, options.clock.now());
|
|
314
|
+
if (before.governanceGeneration !== after.governanceGeneration ||
|
|
315
|
+
before.validationGeneration !== after.validationGeneration)
|
|
316
|
+
throw new RepositoryConflictError();
|
|
317
|
+
const article = await submitCoreReview(stored.article, {
|
|
318
|
+
expectedRevision: stored.article.current.number,
|
|
319
|
+
context: after.context,
|
|
320
|
+
});
|
|
321
|
+
if (article.review === undefined)
|
|
322
|
+
throw new RepositoryIntegrityError();
|
|
323
|
+
const review = {
|
|
324
|
+
articleGeneration: generation,
|
|
325
|
+
binding: article.review,
|
|
326
|
+
sourceSnapshotHash: stored.provenance.hash,
|
|
327
|
+
sourceGeneration: stored.provenance.generation,
|
|
328
|
+
governanceGeneration: after.governanceGeneration,
|
|
329
|
+
};
|
|
330
|
+
return { next: { ...nextState(stored), article, review } };
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
publishArticle(access, input) {
|
|
334
|
+
return execute(access, input, knowledgePermissions.publish, "article.publish", ["review"], async (current, _generation, command, caller) => {
|
|
335
|
+
const stored = requireArticle(current);
|
|
336
|
+
rejectRevokedCurrentEvidence(stored);
|
|
337
|
+
if (stored.review === undefined ||
|
|
338
|
+
stored.review.articleGeneration !== stored.generation ||
|
|
339
|
+
canonicalJson(stored.review) !== canonicalJson(command.review) ||
|
|
340
|
+
stored.review.sourceGeneration !== stored.provenance.generation ||
|
|
341
|
+
stored.review.sourceSnapshotHash !== stored.provenance.hash)
|
|
342
|
+
throw new RepositoryConflictError();
|
|
343
|
+
const before = await loadGovernanceContext(repository, stored.article.current.collectionId, options.clock.now());
|
|
344
|
+
if (before.governanceGeneration !== stored.review.governanceGeneration) {
|
|
345
|
+
throw new RepositoryConflictError();
|
|
346
|
+
}
|
|
347
|
+
await sourcesEligible(stored.article.current, stored.provenance);
|
|
348
|
+
const after = await loadGovernanceContext(repository, stored.article.current.collectionId, options.clock.now());
|
|
349
|
+
if (before.governanceGeneration !== after.governanceGeneration ||
|
|
350
|
+
before.validationGeneration !== after.validationGeneration)
|
|
351
|
+
throw new RepositoryConflictError();
|
|
352
|
+
const article = await publishCoreArticle(stored.article, {
|
|
353
|
+
expectedRevision: stored.article.current.number,
|
|
354
|
+
binding: stored.review.binding,
|
|
355
|
+
reviewerId: caller.principalId,
|
|
356
|
+
context: after.context,
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
next: {
|
|
360
|
+
...withoutPendingReview(stored),
|
|
361
|
+
article,
|
|
362
|
+
publicationHeld: false,
|
|
363
|
+
publishedProvenance: stored.provenance,
|
|
364
|
+
publicationReview: stored.review,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
},
|
|
369
|
+
rejectReview(access, input) {
|
|
370
|
+
return execute(access, input, knowledgePermissions.review, "article.review.reject", ["reason"], async (current, _generation, command) => {
|
|
371
|
+
const stored = requireArticle(current);
|
|
372
|
+
boundedText(command.reason, "reason");
|
|
373
|
+
const article = rejectCoreReview(stored.article, stored.article.current.number);
|
|
374
|
+
return {
|
|
375
|
+
next: { ...withoutPendingReview(stored), article },
|
|
376
|
+
auditDetails: { reason: command.reason },
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
},
|
|
380
|
+
retireArticle(access, input) {
|
|
381
|
+
return execute(access, input, knowledgePermissions.retire, "article.retire", [], async (current) => {
|
|
382
|
+
const stored = requireArticle(current);
|
|
383
|
+
const { publishedProvenance: _publishedProvenance, publicationReview: _publicationReview, ...rest } = withoutPendingReview(stored);
|
|
384
|
+
return {
|
|
385
|
+
next: {
|
|
386
|
+
...rest,
|
|
387
|
+
article: retireCoreArticle(stored.article, stored.article.current.number),
|
|
388
|
+
publicationHeld: true,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}, true);
|
|
392
|
+
},
|
|
393
|
+
setProvenance(access, input) {
|
|
394
|
+
return execute(access, input, knowledgePermissions.provenanceWrite, "article.provenance.set", ["references"], async (current, _generation, command, caller) => {
|
|
395
|
+
const stored = requireArticle(current);
|
|
396
|
+
await requireSourceAccess(caller, command.articleId, command.references);
|
|
397
|
+
const provenance = await sourceSnapshot(stored.provenance.generation + 1, command.references);
|
|
398
|
+
return { next: { ...withoutPendingReview(stored), provenance } };
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
revokeEvidence(access, input) {
|
|
402
|
+
return execute(access, input, knowledgePermissions.evidenceRevoke, "article.evidence.revoke", [
|
|
403
|
+
"articleRevision",
|
|
404
|
+
"evidenceId",
|
|
405
|
+
"evidenceVersion",
|
|
406
|
+
"evidenceHash",
|
|
407
|
+
"reason",
|
|
408
|
+
], async (current, _generation, command, caller) => {
|
|
409
|
+
const stored = requireArticle(current);
|
|
410
|
+
requireGeneration(command.articleRevision, 1);
|
|
411
|
+
requireGeneration(command.evidenceVersion, 1);
|
|
412
|
+
requireIdentifier(command.evidenceId, "evidenceId");
|
|
413
|
+
boundedText(command.reason, "reason");
|
|
414
|
+
if (!/^[a-f0-9]{64}$/u.test(command.evidenceHash))
|
|
415
|
+
throw new TypeError("Evidence hash must be SHA-256");
|
|
416
|
+
const evidence = await repository.getEvidenceVersion(command.articleId, command.articleRevision, command.evidenceId, command.evidenceVersion);
|
|
417
|
+
if (evidence === null || evidence.hash !== command.evidenceHash) {
|
|
418
|
+
throw new KnowledgeBaseNotFoundError("evidence");
|
|
419
|
+
}
|
|
420
|
+
if (stored.revokedEvidence.some((marker) => marker.hash === evidence.hash)) {
|
|
421
|
+
throw new RepositoryConflictError();
|
|
422
|
+
}
|
|
423
|
+
const revocation = {
|
|
424
|
+
articleRevision: evidence.articleRevision,
|
|
425
|
+
id: evidence.id,
|
|
426
|
+
version: evidence.version,
|
|
427
|
+
hash: evidence.hash,
|
|
428
|
+
reason: command.reason,
|
|
429
|
+
revokedBy: caller.principalId,
|
|
430
|
+
revokedAt: options.clock.now(),
|
|
431
|
+
};
|
|
432
|
+
const published = stored.article.published;
|
|
433
|
+
const referencesPublished = published !== undefined &&
|
|
434
|
+
published.revision.number === evidence.articleRevision &&
|
|
435
|
+
published.approval.binding.evidence.some((binding) => binding.id === evidence.id &&
|
|
436
|
+
binding.version === evidence.version &&
|
|
437
|
+
binding.hash === evidence.hash);
|
|
438
|
+
return {
|
|
439
|
+
next: {
|
|
440
|
+
...withoutPendingReview(stored),
|
|
441
|
+
publicationHeld: stored.publicationHeld || referencesPublished,
|
|
442
|
+
revokedEvidence: [...stored.revokedEvidence, revocation],
|
|
443
|
+
},
|
|
444
|
+
auditDetails: {
|
|
445
|
+
evidenceId: evidence.id,
|
|
446
|
+
evidenceVersion: evidence.version,
|
|
447
|
+
reason: command.reason,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}, true);
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
//# sourceMappingURL=articles.js.map
|