@semiont/make-meaning 0.5.2 → 0.5.3

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
@@ -170,6 +170,19 @@ const kb = await createKnowledgeBase(eventStore, project, graphDb, logger);
170
170
 
171
171
  The EventBus is created by the backend (or script) and passed into `startMakeMeaning()` as a dependency. Make-meaning does not own or encapsulate the EventBus — it is shared across the entire system.
172
172
 
173
+ ### Pure projection validators
174
+
175
+ The dispatcher in [`src/handlers/job-commands.ts`](src/handlers/job-commands.ts) does projection-validated job creation: when a `mark.assist` (linking) or `yield.fromAnnotation` job arrives with `entityTypes`, the dispatcher validates that every tag is registered; when a tagging job arrives with a `schemaId`, the dispatcher resolves it against the registered tag-schema set.
176
+
177
+ Both rules are pure functions in [`src/views/projection-validators.ts`](src/views/projection-validators.ts):
178
+
179
+ - `resolveTagSchema(schemas, schemaId)` → `{ schema } | { error }` — id lookup with the standard "Tag schema not registered" / "tag-annotation requires schemaId" error formats.
180
+ - `validateEntityTypes(registered, requested)` → `{ ok: true } | { ok: false; unknown }` — set membership check that lists the offending tags in caller order.
181
+
182
+ The dispatcher is the I/O shell: read the projection (via the readers in `src/views/`), pass it to the validator (pure), then either stash the resolved value or rethrow as `job:create-failed`. Validator unit tests run in single-digit milliseconds with no filesystem, no event-bus, no mock JobQueue — the dispatcher integration tests in `__tests__/handlers/job-commands.test.ts` keep the wiring covered.
183
+
184
+ This pattern (functional core, imperative shell) is shared with `@semiont/event-sourcing`'s projection reducers; see [`docs/system/PROJECTION-PATTERN.md`](../../docs/system/PROJECTION-PATTERN.md) for the architectural narrative, the full axiom catalog, and guidance for adding new validators.
185
+
173
186
  ## Documentation
174
187
 
175
188
  - **[Architecture](./docs/architecture.md)** — Actor model, data flow, storage architecture
package/dist/index.d.ts CHANGED
@@ -215,6 +215,7 @@ declare function createKnowledgeBase(eventStore: EventStore, project: SemiontPro
215
215
  * - mark:archive → resource.archived (+ file removal) (resource-scoped, no result event)
216
216
  * - mark:unarchive → resource.unarchived (resource-scoped, no result event)
217
217
  * - frame:add-entity-type → entitytype.added → frame:entity-type-added / frame:entity-type-add-failed
218
+ * - frame:add-tag-schema → tagschema.added → frame:tag-schema-added / frame:tag-schema-add-failed
218
219
  * - mark:update-entity-types → entitytag.added / entitytag.removed
219
220
  * - job:start → job.started
220
221
  * - job:complete → job.completed
@@ -247,6 +248,7 @@ declare class Stower {
247
248
  private handleMarkArchive;
248
249
  private handleMarkUnarchive;
249
250
  private handleAddEntityType;
251
+ private handleAddTagSchema;
250
252
  private handleUpdateEntityTypes;
251
253
  private handleJobStart;
252
254
  private handleJobComplete;
@@ -366,6 +368,7 @@ declare class Matcher {
366
368
  * - browse:annotation-history-requested — annotation event history
367
369
  * - browse:referenced-by-requested — find annotations in the KB graph that reference a resource
368
370
  * - browse:entity-types-requested — list entity types from the project projection
371
+ * - browse:tag-schemas-requested — list tag schemas from the project projection
369
372
  * - browse:directory-requested — list a project directory, merging fs + ViewStorage
370
373
  */
371
374
 
@@ -386,6 +389,7 @@ declare class Browser {
386
389
  private handleBrowseAnnotationHistory;
387
390
  private handleReferencedBy;
388
391
  private handleEntityTypes;
392
+ private handleTagSchemas;
389
393
  private handleBrowseDirectory;
390
394
  stop(): Promise<void>;
391
395
  }
@@ -617,7 +621,7 @@ declare function registerAnnotationLookupHandlers(eventBus: EventBus, kb: Knowle
617
621
  */
618
622
  declare function registerBindUpdateBodyHandler(eventBus: EventBus, parentLogger: Logger): void;
619
623
 
620
- declare function registerJobCommandHandlers(eventBus: EventBus, jobQueue: JobQueue, parentLogger: Logger): void;
624
+ declare function registerJobCommandHandlers(eventBus: EventBus, jobQueue: JobQueue, project: SemiontProject, parentLogger: Logger): void;
621
625
 
622
626
  /**
623
627
  * Bus command handlers — pure bus-event translators that bridge the
@@ -636,7 +640,7 @@ declare function registerJobCommandHandlers(eventBus: EventBus, jobQueue: JobQue
636
640
  * Register all bus command handlers on the make-meaning EventBus. Called
637
641
  * during `startMakeMeaning` after the JobQueue and KnowledgeSystem exist.
638
642
  */
639
- declare function registerBusHandlers(eventBus: EventBus, knowledgeSystem: KnowledgeSystem, jobQueue: JobQueue, logger: Logger): void;
643
+ declare function registerBusHandlers(eventBus: EventBus, knowledgeSystem: KnowledgeSystem, jobQueue: JobQueue, project: SemiontProject, logger: Logger): void;
640
644
 
641
645
  /**
642
646
  * Entity Types Bootstrap
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { getGraphDatabase } from '@semiont/graph';
8
8
  import { WorkingTreeStore, deriveStorageUri, getExtensionForMimeType } from '@semiont/content';
9
9
  import { getEntityTypes, DEFAULT_ENTITY_TYPES } from '@semiont/ontology';
10
10
  import { promises } from 'fs';
11
- import * as path2 from 'path';
11
+ import * as path3 from 'path';
12
12
  import { createGzip, createGunzip } from 'zlib';
13
13
  import { pipeline, Readable } from 'stream';
14
14
  import { promisify } from 'util';
@@ -11347,6 +11347,7 @@ var Stower = class {
11347
11347
  pipe("mark:delete", (e) => this.handleMarkDelete(e)),
11348
11348
  pipe("mark:update-body", (e) => this.handleMarkUpdateBody(e)),
11349
11349
  pipe("frame:add-entity-type", (e) => this.handleAddEntityType(e)),
11350
+ pipe("frame:add-tag-schema", (e) => this.handleAddTagSchema(e)),
11350
11351
  pipe("mark:archive", (e) => this.handleMarkArchive(e)),
11351
11352
  pipe("mark:unarchive", (e) => this.handleMarkUnarchive(e)),
11352
11353
  pipe("mark:update-entity-types", (e) => this.handleUpdateEntityTypes(e)),
@@ -11634,6 +11635,24 @@ var Stower = class {
11634
11635
  });
11635
11636
  }
11636
11637
  }
11638
+ async handleAddTagSchema(event) {
11639
+ if (!event._userId) {
11640
+ throw new Error("frame:add-tag-schema missing _userId (gateway injection)");
11641
+ }
11642
+ try {
11643
+ await this.kb.eventStore.appendEvent({
11644
+ type: "frame:tag-schema-added",
11645
+ userId: userId(event._userId),
11646
+ version: 1,
11647
+ payload: { schema: event.schema }
11648
+ });
11649
+ } catch (error) {
11650
+ this.logger.error("Failed to add tag schema", { schemaId: event.schema?.id, error: errField(error) });
11651
+ this.eventBus.get("frame:tag-schema-add-failed").next({
11652
+ message: error instanceof Error ? error.message : String(error)
11653
+ });
11654
+ }
11655
+ }
11637
11656
  async handleUpdateEntityTypes(event) {
11638
11657
  if (!event._userId) {
11639
11658
  throw new Error("mark:update-entity-types missing _userId (gateway injection)");
@@ -11721,7 +11740,7 @@ var Stower = class {
11721
11740
  var import_rxjs5 = __toESM(require_cjs());
11722
11741
  var import_operators5 = __toESM(require_operators());
11723
11742
  async function readEntityTypesProjection(project) {
11724
- const entityTypesPath = path2.join(
11743
+ const entityTypesPath = path3.join(
11725
11744
  project.stateDir,
11726
11745
  "projections",
11727
11746
  "__system__",
@@ -11738,6 +11757,24 @@ async function readEntityTypesProjection(project) {
11738
11757
  throw error;
11739
11758
  }
11740
11759
  }
11760
+ async function readTagSchemasProjection(project) {
11761
+ const tagSchemasPath = path3.join(
11762
+ project.stateDir,
11763
+ "projections",
11764
+ "__system__",
11765
+ "tagschemas.json"
11766
+ );
11767
+ try {
11768
+ const content = await promises.readFile(tagSchemasPath, "utf-8");
11769
+ const projection = JSON.parse(content);
11770
+ return projection.tagSchemas || [];
11771
+ } catch (error) {
11772
+ if (error.code === "ENOENT") {
11773
+ return [];
11774
+ }
11775
+ throw error;
11776
+ }
11777
+ }
11741
11778
 
11742
11779
  // src/browser.ts
11743
11780
  var Browser = class {
@@ -11767,6 +11804,7 @@ var Browser = class {
11767
11804
  pipe("browse:annotation-history-requested", (e) => this.handleBrowseAnnotationHistory(e)).subscribe({ error: errorHandler }),
11768
11805
  pipe("browse:referenced-by-requested", (e) => this.handleReferencedBy(e)).subscribe({ error: errorHandler }),
11769
11806
  pipe("browse:entity-types-requested", (e) => this.handleEntityTypes(e)).subscribe({ error: errorHandler }),
11807
+ pipe("browse:tag-schemas-requested", (e) => this.handleTagSchemas(e)).subscribe({ error: errorHandler }),
11770
11808
  pipe("browse:directory-requested", (e) => this.handleBrowseDirectory(e)).subscribe({ error: errorHandler })
11771
11809
  );
11772
11810
  }
@@ -12011,14 +12049,29 @@ var Browser = class {
12011
12049
  });
12012
12050
  }
12013
12051
  }
12052
+ async handleTagSchemas(event) {
12053
+ try {
12054
+ const tagSchemas = await readTagSchemasProjection(this.project);
12055
+ this.eventBus.get("browse:tag-schemas-result").next({
12056
+ correlationId: event.correlationId,
12057
+ response: { tagSchemas }
12058
+ });
12059
+ } catch (error) {
12060
+ this.logger.error("Tag schemas read failed", { error: errField(error) });
12061
+ this.eventBus.get("browse:tag-schemas-failed").next({
12062
+ correlationId: event.correlationId,
12063
+ message: error instanceof Error ? error.message : String(error)
12064
+ });
12065
+ }
12066
+ }
12014
12067
  // ========================================================================
12015
12068
  // Filesystem read handler
12016
12069
  // ========================================================================
12017
12070
  async handleBrowseDirectory(event) {
12018
12071
  const { correlationId, path: reqPath, sort = "name" } = event;
12019
12072
  const projectRoot = this.project.root;
12020
- const resolved = path2.resolve(projectRoot, reqPath);
12021
- if (!resolved.startsWith(projectRoot + path2.sep) && resolved !== projectRoot) {
12073
+ const resolved = path3.resolve(projectRoot, reqPath);
12074
+ if (!resolved.startsWith(projectRoot + path3.sep) && resolved !== projectRoot) {
12022
12075
  this.eventBus.get("browse:directory-failed").next({
12023
12076
  correlationId,
12024
12077
  path: reqPath,
@@ -12042,12 +12095,12 @@ var Browser = class {
12042
12095
  const allViews = await this.views.getAll();
12043
12096
  const prefix = `file://${resolved}`;
12044
12097
  const viewsByUri = new Map(
12045
- allViews.filter((v) => v.resource.storageUri?.startsWith(prefix + "/") || v.resource.storageUri?.startsWith(prefix + path2.sep)).map((v) => [v.resource.storageUri, v])
12098
+ allViews.filter((v) => v.resource.storageUri?.startsWith(prefix + "/") || v.resource.storageUri?.startsWith(prefix + path3.sep)).map((v) => [v.resource.storageUri, v])
12046
12099
  );
12047
12100
  const entries = [];
12048
12101
  for (const dirent of visible) {
12049
- const entryPath = path2.join(resolved, dirent.name);
12050
- const relPath = path2.relative(projectRoot, entryPath);
12102
+ const entryPath = path3.join(resolved, dirent.name);
12103
+ const relPath = path3.relative(projectRoot, entryPath);
12051
12104
  if (dirent.isDirectory()) {
12052
12105
  let mtime = (/* @__PURE__ */ new Date(0)).toISOString();
12053
12106
  try {
@@ -12585,6 +12638,31 @@ function registerBindUpdateBodyHandler(eventBus, parentLogger) {
12585
12638
  logger.warn("Bind body-update failed after forwarding", { correlationId: cid, message });
12586
12639
  });
12587
12640
  }
12641
+
12642
+ // src/views/projection-validators.ts
12643
+ function resolveTagSchema(tagSchemas, schemaId) {
12644
+ if (typeof schemaId !== "string" || !schemaId) {
12645
+ return { error: "tag-annotation requires schemaId" };
12646
+ }
12647
+ const schema = tagSchemas.find((s) => s.id === schemaId);
12648
+ if (!schema) {
12649
+ return { error: `Tag schema not registered: ${schemaId}` };
12650
+ }
12651
+ return { schema };
12652
+ }
12653
+ function validateEntityTypes(registered, requested) {
12654
+ if (!requested || requested.length === 0) {
12655
+ return { ok: true };
12656
+ }
12657
+ const set = new Set(registered);
12658
+ const unknown = requested.filter((t) => !set.has(t));
12659
+ return unknown.length > 0 ? { ok: false, unknown } : { ok: true };
12660
+ }
12661
+ function entityTypesNotRegisteredMessage(unknown) {
12662
+ return `Entity type not registered: ${unknown.join(", ")}`;
12663
+ }
12664
+
12665
+ // src/handlers/job-commands.ts
12588
12666
  function parseDidUser(did) {
12589
12667
  const parts = did.split(":");
12590
12668
  const usersIdx = parts.indexOf("users");
@@ -12592,7 +12670,7 @@ function parseDidUser(did) {
12592
12670
  const email = decodeURIComponent(parts.slice(usersIdx + 1).join(":"));
12593
12671
  return { userId: did, email, domain };
12594
12672
  }
12595
- function registerJobCommandHandlers(eventBus, jobQueue, parentLogger) {
12673
+ function registerJobCommandHandlers(eventBus, jobQueue, project, parentLogger) {
12596
12674
  const logger = parentLogger.child({ component: "job-commands" });
12597
12675
  eventBus.get("job:create").subscribe(async (command) => {
12598
12676
  const { correlationId, jobType, resourceId: resId, params, _userId } = command;
@@ -12620,9 +12698,25 @@ function registerJobCommandHandlers(eventBus, jobQueue, parentLogger) {
12620
12698
  }
12621
12699
  };
12622
12700
  const jobParams = job.params;
12701
+ if ((jobType === "reference-annotation" || jobType === "generation") && Array.isArray(jobParams.entityTypes) && jobParams.entityTypes.length > 0) {
12702
+ const registered = await readEntityTypesProjection(project);
12703
+ const result = validateEntityTypes(registered, jobParams.entityTypes);
12704
+ if (!result.ok) {
12705
+ throw new Error(entityTypesNotRegisteredMessage(result.unknown));
12706
+ }
12707
+ }
12623
12708
  if (jobType === "reference-annotation" && jobParams.entityTypes) {
12624
12709
  jobParams.entityTypes = jobParams.entityTypes.map((et) => entityType(et));
12625
12710
  }
12711
+ if (jobType === "tag-annotation") {
12712
+ const schemas = await readTagSchemasProjection(project);
12713
+ const result = resolveTagSchema(schemas, jobParams.schemaId);
12714
+ if (result.error !== void 0) {
12715
+ throw new Error(result.error);
12716
+ }
12717
+ jobParams.schema = result.schema;
12718
+ delete jobParams.schemaId;
12719
+ }
12626
12720
  await jobQueue.createJob(job);
12627
12721
  logger.info("Job created via bus", { jobId: job.metadata.id, jobType, correlationId });
12628
12722
  eventBus.get("job:created").next({
@@ -12668,11 +12762,11 @@ function registerJobCommandHandlers(eventBus, jobQueue, parentLogger) {
12668
12762
  }
12669
12763
 
12670
12764
  // src/handlers/index.ts
12671
- function registerBusHandlers(eventBus, knowledgeSystem, jobQueue, logger) {
12765
+ function registerBusHandlers(eventBus, knowledgeSystem, jobQueue, project, logger) {
12672
12766
  registerAnnotationAssemblyHandler(eventBus, logger);
12673
12767
  registerAnnotationLookupHandlers(eventBus, knowledgeSystem.kb, knowledgeSystem.gatherer, logger);
12674
12768
  registerBindUpdateBodyHandler(eventBus, logger);
12675
- registerJobCommandHandlers(eventBus, jobQueue, logger);
12769
+ registerJobCommandHandlers(eventBus, jobQueue, project, logger);
12676
12770
  }
12677
12771
 
12678
12772
  // src/service.ts
@@ -12783,7 +12877,7 @@ async function startMakeMeaning(project, config, eventBus, logger, options) {
12783
12877
  const skipRebuild = options?.skipRebuild ?? process.env.SEMIONT_SKIP_REBUILD === "true";
12784
12878
  const { jobQueue, jobStatusSubscription } = await createJobQueue(project, eventBus, logger);
12785
12879
  const knowledgeSystem = await createKnowledgeSystemFromConfig(project, config, eventBus, logger, skipRebuild);
12786
- registerBusHandlers(eventBus, knowledgeSystem, jobQueue, logger);
12880
+ registerBusHandlers(eventBus, knowledgeSystem, jobQueue, project, logger);
12787
12881
  return {
12788
12882
  knowledgeSystem,
12789
12883
  jobQueue,