@hyperscale0/hsx 2.0.3 → 2.0.4

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.
@@ -191,6 +191,7 @@ export function checkGeneralProgram(program, options = {}) {
191
191
  concrete.push(...bindActionPorts(constructedInstruments(decl.name, body, diagnostics), scoped.ports, reachedPorts).map((candidate) => ({
192
192
  ...candidate,
193
193
  aliases: scoped.aliases,
194
+ callee: candidate.name.name,
194
195
  parties: scoped.parties,
195
196
  })));
196
197
  }
@@ -218,11 +219,19 @@ export function checkGeneralProgram(program, options = {}) {
218
219
  const merged = decl.metadata
219
220
  ? mergeApplicationMetadata(body, decl.metadata, diagnostics)
220
221
  : body;
221
- concrete.push(...bindActionPorts(constructedInstruments(decl.name, merged, diagnostics), boundPorts, applicationPorts).map((candidate) => ({
222
- ...candidate,
223
- aliases: scoped.aliases,
224
- parties: scoped.parties,
225
- })));
222
+ concrete.push(...bindActionPorts(constructedInstruments(decl.name, merged, diagnostics), boundPorts, applicationPorts).map((candidate) => {
223
+ let candidateCallee = callee;
224
+ if (candidate.name.name !== decl.name.name &&
225
+ candidate.name.name.startsWith(`${decl.name.name}_`)) {
226
+ candidateCallee = `${callee}${candidate.name.name.slice(decl.name.name.length)}`;
227
+ }
228
+ return {
229
+ ...candidate,
230
+ aliases: scoped.aliases,
231
+ callee: candidateCallee,
232
+ parties: scoped.parties,
233
+ };
234
+ }));
226
235
  for (const port of boundPorts.values()) {
227
236
  if (applicationPorts.has(port))
228
237
  reachedPorts.add(port);
@@ -237,6 +246,9 @@ export function checkGeneralProgram(program, options = {}) {
237
246
  report("HSX1024", port.name.span, `wired decision port ${port.name.name} reaches no action`, `carry ${port.name.name}'s condition into the action it authorizes`);
238
247
  }
239
248
  const allocated = allocateGeneratedPrefixes(concrete);
249
+ if (options.publishedCatalog) {
250
+ checkPortCaptureTypes(allocated, options.publishedCatalog, aliases, diagnostics);
251
+ }
240
252
  const instruments = [];
241
253
  const ids = new Set();
242
254
  for (const candidate of allocated) {
@@ -532,6 +544,115 @@ function publishedFieldType(schema, catalog) {
532
544
  `^${instrument.idPrefix}_(sandbox|live)_[a-z0-9]{8,64}$`);
533
545
  return target ? { kind: "ref", target: target.id } : { kind: "text" };
534
546
  }
547
+ const STRING_BACKED_PORT_KINDS = new Set([
548
+ "account",
549
+ "condition",
550
+ "date",
551
+ "party",
552
+ "ref",
553
+ "text",
554
+ ]);
555
+ const INTEGER_BACKED_PORT_KINDS = new Set([
556
+ "bps",
557
+ "integer",
558
+ "percent",
559
+ ]);
560
+ /**
561
+ * A published schema keeps less than the HSX type: dates and accounts publish
562
+ * as plain strings, percents and bps as integers. Compare by the family the
563
+ * schema can still express so the check never rejects a richer declared type.
564
+ */
565
+ function samePortType(declared, captured) {
566
+ if (captured.kind === "unknown" || declared.kind === "unknown")
567
+ return true;
568
+ if (captured.kind === "text")
569
+ return STRING_BACKED_PORT_KINDS.has(declared.kind);
570
+ if (captured.kind === "integer") {
571
+ return INTEGER_BACKED_PORT_KINDS.has(declared.kind);
572
+ }
573
+ if (declared.kind !== captured.kind)
574
+ return false;
575
+ if (captured.currency && declared.currency !== captured.currency) {
576
+ return false;
577
+ }
578
+ if (captured.target && declared.target !== captured.target) {
579
+ return false;
580
+ }
581
+ return true;
582
+ }
583
+ function portTypeWords(type) {
584
+ if (type.kind === "ref" && type.target)
585
+ return `ref<${type.target}>`;
586
+ return typeWords(type);
587
+ }
588
+ function getFieldSchemaFromAction(action, fieldName) {
589
+ if (!action.captureInput)
590
+ return undefined;
591
+ const properties = action.input?.properties;
592
+ if (!properties)
593
+ return undefined;
594
+ for (const inputKey of Object.values(action.captureInput)) {
595
+ if (inputKey !== fieldName && camel(inputKey) !== camel(fieldName)) {
596
+ continue;
597
+ }
598
+ const schema = properties[inputKey] ?? properties[camel(inputKey)];
599
+ if (schema && typeof schema === "object")
600
+ return schema;
601
+ }
602
+ return undefined;
603
+ }
604
+ function findCapturedFieldSchema(catalogInstrument, actionName, fieldName) {
605
+ const targetAction = catalogInstrument.actions[actionName];
606
+ return targetAction
607
+ ? getFieldSchemaFromAction(targetAction, fieldName)
608
+ : undefined;
609
+ }
610
+ function checkPortCaptureTypes(candidates, publishedCatalog, topLevelAliases, diagnostics) {
611
+ const catalogById = new Map(publishedCatalog.instruments.map((instrument) => [
612
+ instrument.id,
613
+ instrument,
614
+ ]));
615
+ for (const candidate of candidates) {
616
+ const catalogInstrument = catalogById.get(candidate.name.name) ??
617
+ (candidate.callee ? catalogById.get(candidate.callee) : undefined);
618
+ if (!catalogInstrument)
619
+ continue;
620
+ const aliases = new Map([...topLevelAliases, ...candidate.aliases]);
621
+ for (const [actionName, declaredPorts] of candidate.ports) {
622
+ for (const port of declaredPorts) {
623
+ const rawShape = entry(port.body, "shape")?.value;
624
+ if (rawShape?.kind !== "block")
625
+ continue;
626
+ for (const fieldEntry of rawShape.entries) {
627
+ const fieldName = fieldEntry.key.name;
628
+ const capturedSchema = findCapturedFieldSchema(catalogInstrument, actionName, fieldName);
629
+ if (!capturedSchema)
630
+ continue;
631
+ const capturedType = publishedFieldType(capturedSchema, publishedCatalog.instruments);
632
+ const declaredType = typeOf(fieldEntry.value, aliases);
633
+ if (!samePortType(declaredType, capturedType)) {
634
+ const declaredWords = portTypeWords(declaredType);
635
+ const capturedWords = portTypeWords(capturedType);
636
+ const message = `decision port ${port.name.name} field ${fieldName} declares ${declaredWords} but instrument ${catalogInstrument.id} captures it as ${capturedWords}`;
637
+ const fix = `declare ${fieldName} as ${capturedWords} in decision port ${port.name.name}`;
638
+ if (!diagnostics.some((prior) => prior.code === "HSX1026" &&
639
+ prior.span.start === fieldEntry.value.span.start &&
640
+ prior.span.end === fieldEntry.value.span.end &&
641
+ prior.message === message)) {
642
+ diagnostics.push({
643
+ code: "HSX1026",
644
+ fix,
645
+ message,
646
+ severity: "error",
647
+ span: fieldEntry.value.span,
648
+ });
649
+ }
650
+ }
651
+ }
652
+ }
653
+ }
654
+ }
655
+ }
535
656
  function typedPublishedSubject(subject, origin) {
536
657
  return {
537
658
  declaredValue: subject.declaredValue,