@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -2
  3. package/dist/articles.d.ts +10 -0
  4. package/dist/articles.d.ts.map +1 -0
  5. package/dist/articles.js +454 -0
  6. package/dist/articles.js.map +1 -0
  7. package/dist/errors.d.ts +18 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +34 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/governance.d.ts +14 -0
  12. package/dist/governance.d.ts.map +1 -0
  13. package/dist/governance.js +364 -0
  14. package/dist/governance.js.map +1 -0
  15. package/dist/index.d.ts +13 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +46 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/lexical.d.ts +13 -0
  20. package/dist/lexical.d.ts.map +1 -0
  21. package/dist/lexical.js +105 -0
  22. package/dist/lexical.js.map +1 -0
  23. package/dist/publication.d.ts +42 -0
  24. package/dist/publication.d.ts.map +1 -0
  25. package/dist/publication.js +258 -0
  26. package/dist/publication.js.map +1 -0
  27. package/dist/reads.d.ts +5 -0
  28. package/dist/reads.d.ts.map +1 -0
  29. package/dist/reads.js +228 -0
  30. package/dist/reads.js.map +1 -0
  31. package/dist/records.d.ts +40 -0
  32. package/dist/records.d.ts.map +1 -0
  33. package/dist/records.js +122 -0
  34. package/dist/records.js.map +1 -0
  35. package/dist/repository.d.ts +74 -0
  36. package/dist/repository.d.ts.map +1 -0
  37. package/dist/repository.js +738 -0
  38. package/dist/repository.js.map +1 -0
  39. package/dist/retrieval-budget.d.ts +37 -0
  40. package/dist/retrieval-budget.d.ts.map +1 -0
  41. package/dist/retrieval-budget.js +111 -0
  42. package/dist/retrieval-budget.js.map +1 -0
  43. package/dist/retrieval-limits.d.ts +4 -0
  44. package/dist/retrieval-limits.d.ts.map +1 -0
  45. package/dist/retrieval-limits.js +21 -0
  46. package/dist/retrieval-limits.js.map +1 -0
  47. package/dist/retrieval-maintenance.d.ts +19 -0
  48. package/dist/retrieval-maintenance.d.ts.map +1 -0
  49. package/dist/retrieval-maintenance.js +359 -0
  50. package/dist/retrieval-maintenance.js.map +1 -0
  51. package/dist/retrieval-records.d.ts +55 -0
  52. package/dist/retrieval-records.d.ts.map +1 -0
  53. package/dist/retrieval-records.js +140 -0
  54. package/dist/retrieval-records.js.map +1 -0
  55. package/dist/retrieval-repository.d.ts +32 -0
  56. package/dist/retrieval-repository.d.ts.map +1 -0
  57. package/dist/retrieval-repository.js +750 -0
  58. package/dist/retrieval-repository.js.map +1 -0
  59. package/dist/retrieval-types.d.ts +185 -0
  60. package/dist/retrieval-types.d.ts.map +1 -0
  61. package/dist/retrieval-types.js +24 -0
  62. package/dist/retrieval-types.js.map +1 -0
  63. package/dist/retrieval.d.ts +12 -0
  64. package/dist/retrieval.d.ts.map +1 -0
  65. package/dist/retrieval.js +541 -0
  66. package/dist/retrieval.js.map +1 -0
  67. package/dist/storage-helpers.d.ts +23 -0
  68. package/dist/storage-helpers.d.ts.map +1 -0
  69. package/dist/storage-helpers.js +138 -0
  70. package/dist/storage-helpers.js.map +1 -0
  71. package/dist/types.d.ts +280 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +2 -0
  74. package/dist/types.js.map +1 -0
  75. 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
- # Temporary Holding Version
1
+ # @pegma/knowledge-base-application
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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"}
@@ -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