@ductape/sdk 0.3.6 → 0.3.8

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.
@@ -20,6 +20,8 @@ export interface DuctapeAssetSpan {
20
20
  error_message?: string;
21
21
  }
22
22
  export interface DuctapeExecutionContext {
23
+ /** Probe internals are represented by one healthcheck observation, not API logs. */
24
+ healthcheck_probe?: boolean;
23
25
  workspace_id?: string;
24
26
  product_id?: string;
25
27
  product_tag?: string;
@@ -33,7 +33,7 @@ function runWithExecutionContext(context, fn) {
33
33
  function executionContextFields(context = currentExecutionContext()) {
34
34
  if (!context)
35
35
  return {};
36
- const { session: _secretSession, baggage: _baggage, asset_span_sink: _assetSpanSink, kind: execution_kind } = context, fields = __rest(context, ["session", "baggage", "asset_span_sink", "kind"]);
36
+ const { session: _secretSession, baggage: _baggage, asset_span_sink: _assetSpanSink, healthcheck_probe: _healthcheckProbe, kind: execution_kind } = context, fields = __rest(context, ["session", "baggage", "asset_span_sink", "healthcheck_probe", "kind"]);
37
37
  return Object.assign(Object.assign({}, fields), { execution_kind });
38
38
  }
39
39
  async function withAssetSpan(asset, fn) {
@@ -1 +1 @@
1
- {"version":3,"file":"execution-context.js","sourceRoot":"","sources":["../../src/context/execution-context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"execution-context.js","sourceRoot":"","sources":["../../src/context/execution-context.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAgEA,0DAEC;AAED,wDAYC;AAED,0DAEC;AAGD,wDAWC;AAED,sCAwDC;AA5JD,6CAAgD;AAChD,mCAAoC;AA6DpC,MAAM,OAAO,GAAG,IAAI,+BAAiB,EAA2B,CAAC;AAEjE,SAAgB,uBAAuB;IACrC,OAAO,OAAO,CAAC,QAAQ,EAAE,CAAC;AAC5B,CAAC;AAED,SAAgB,sBAAsB,CACpC,MAAwC,EACxC,SAA8C,uBAAuB,EAAE;;IAEvE,qDACK,MAAM,GACN,MAAM,KACT,IAAI,EAAE,MAAA,MAAA,MAAM,CAAC,IAAI,mCAAI,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,mCAAI,WAAW,EAChD,QAAQ,EAAE,MAAA,MAAA,MAAM,CAAC,QAAQ,mCAAI,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,QAAQ,mCAAI,IAAA,mBAAU,GAAE,EAC7D,OAAO,EAAE,MAAA,MAAM,CAAC,OAAO,mCAAI,IAAA,mBAAU,GAAE,EACvC,cAAc,EAAE,MAAA,MAAM,CAAC,cAAc,mCAAI,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,OAAO,IACxD;AACJ,CAAC;AAED,SAAgB,uBAAuB,CAAI,OAAgC,EAAE,EAAW;IACtF,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,8DAA8D;AAC9D,SAAgB,sBAAsB,CAAC,OAAO,GAAG,uBAAuB,EAAE;IACxE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,EACJ,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,QAAQ,EACjB,eAAe,EAAE,cAAc,EAC/B,iBAAiB,EAAE,iBAAiB,EACpC,IAAI,EAAE,cAAc,KAElB,OAAO,EADN,MAAM,UACP,OAAO,EAPL,sEAOL,CAAU,CAAC;IACZ,uCAAY,MAAM,KAAE,cAAc,IAAG;AACvC,CAAC;AAEM,KAAK,UAAU,aAAa,CACjC,KAAwE,EACxE,EAAoB;IAEpB,MAAM,MAAM,GAAG,uBAAuB,EAAE,CAAC;IACzC,IAAI,CAAC,CAAA,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,eAAe,CAAA;QAAE,OAAO,EAAE,EAAE,CAAC;IAE1C,MAAM,OAAO,GAAG,sBAAsB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;IAClE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,uBAAuB,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,sBAAsB,EAAE,OAAO,CAAC,sBAAsB;YACtD,UAAU,EAAE,KAAK,CAAC,IAAI;YACtB,SAAS,EAAE,KAAK,CAAC,GAAG;YACpB,eAAe,EAAE,KAAK,CAAC,SAAS;YAChC,YAAY,EAAE,KAAK,CAAC,MAAM;YAC1B,MAAM,EAAE,SAAS;YACjB,KAAK;YACL,GAAG;YACH,WAAW,EAAE,GAAG,GAAG,KAAK;SACzB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,sBAAsB,EAAE,OAAO,CAAC,sBAAsB;YACtD,UAAU,EAAE,KAAK,CAAC,IAAI;YACtB,SAAS,EAAE,KAAK,CAAC,GAAG;YACpB,eAAe,EAAE,KAAK,CAAC,SAAS;YAChC,YAAY,EAAE,KAAK,CAAC,MAAM;YAC1B,MAAM,EAAE,MAAM;YACd,KAAK;YACL,GAAG;YACH,WAAW,EAAE,GAAG,GAAG,KAAK;YACxB,aAAa,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SACtE,CAAC,CAAC;QACH,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -24,6 +24,20 @@ export declare class FeatureError extends Error {
24
24
  * Main Feature Service class
25
25
  * Provides unified interface for feature management and execution
26
26
  */
27
+ export interface FeaturePublicationReceipt {
28
+ tag: string;
29
+ operation?: 'create' | 'update';
30
+ write: 'not_attempted' | 'unknown' | 'acknowledged';
31
+ verified: boolean;
32
+ compensation: 'not_needed' | 'retained' | 'unknown' | 'restored';
33
+ }
34
+ export declare class FeaturePublicationError extends Error {
35
+ readonly product: string;
36
+ readonly receipts: FeaturePublicationReceipt[];
37
+ readonly cause: unknown;
38
+ readonly code = "FEATURE_PUBLICATION_FAILED";
39
+ constructor(message: string, product: string, receipts: FeaturePublicationReceipt[], cause: unknown);
40
+ }
27
41
  export declare class FeatureService {
28
42
  /** Service configuration */
29
43
  private config;
@@ -31,6 +45,8 @@ export declare class FeatureService {
31
45
  private productBuilders;
32
46
  /** Local feature configurations */
33
47
  private localConfigs;
48
+ private localKey;
49
+ private localFeatures;
34
50
  /** Feature API service for backend communication */
35
51
  private featureApiService;
36
52
  /** LogService instance for logging operations */
@@ -207,8 +223,9 @@ export declare class FeatureService {
207
223
  /** Compile several definitions without performing any remote writes. */
208
224
  compileMany(definitions: IDefineFeatureOptions<any, any>[]): Promise<IProductFeature[]>;
209
225
  /**
210
- * Publish a definition set with read-back verification. If a write fails, newly
211
- * created records are removed and pre-existing records are restored.
226
+ * Publish with content verification and best-effort restoration of existing records.
227
+ * New records are retained. Failures expose per-feature receipts on the error;
228
+ * this is not an atomic transaction, and rejected writes may have persisted.
212
229
  */
213
230
  publishMany(productTag: string, definitions: IDefineFeatureOptions<any, any>[], options?: {
214
231
  dryRun?: boolean;
@@ -216,6 +233,7 @@ export declare class FeatureService {
216
233
  product: string;
217
234
  dry_run: boolean;
218
235
  features: IProductFeature[];
236
+ receipts: FeaturePublicationReceipt[];
219
237
  }>;
220
238
  /**
221
239
  * Execute a feature
@@ -45,7 +45,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  };
46
46
  var _a, _b;
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.featureService = exports.FeatureCompilationError = exports.FeatureService = exports.FeatureError = void 0;
48
+ exports.featureService = exports.FeatureCompilationError = exports.FeatureService = exports.FeaturePublicationError = exports.FeatureError = void 0;
49
49
  const crypto_1 = require("crypto");
50
50
  const feature_control_flow_validator_1 = require("./feature-control-flow.validator");
51
51
  const functions_1 = require("../functions");
@@ -110,11 +110,26 @@ class FeatureError extends Error {
110
110
  }
111
111
  }
112
112
  exports.FeatureError = FeatureError;
113
- /**
114
- * Main Feature Service class
115
- * Provides unified interface for feature management and execution
116
- */
113
+ class FeaturePublicationError extends Error {
114
+ constructor(message, product, receipts, cause) {
115
+ super(message);
116
+ this.product = product;
117
+ this.receipts = receipts;
118
+ this.cause = cause;
119
+ this.code = 'FEATURE_PUBLICATION_FAILED';
120
+ this.name = 'FeaturePublicationError';
121
+ }
122
+ }
123
+ exports.FeaturePublicationError = FeaturePublicationError;
117
124
  class FeatureService {
125
+ localKey(tag, product) {
126
+ return JSON.stringify([product !== null && product !== void 0 ? product : null, tag]);
127
+ }
128
+ localFeatures(product) {
129
+ return [...this.localConfigs.entries()]
130
+ .filter(([key]) => JSON.parse(key)[0] === (product !== null && product !== void 0 ? product : null))
131
+ .map(([, feature]) => feature);
132
+ }
118
133
  /**
119
134
  * Create a new FeatureService instance
120
135
  * @param config - Optional configuration for authentication and workspace context
@@ -443,7 +458,7 @@ class FeatureService {
443
458
  options: featureData.options,
444
459
  envs: featureData.envs,
445
460
  };
446
- this.localConfigs.set(featureData.tag, localConfig);
461
+ this.localConfigs.set(this.localKey(featureData.tag, productTag), localConfig);
447
462
  console.log('[FeatureService] create SUCCESS', { productTag, tag: featureData.tag });
448
463
  return persisted;
449
464
  }
@@ -468,7 +483,7 @@ class FeatureService {
468
483
  if (!productTag) {
469
484
  const localCount = this.localConfigs.size;
470
485
  console.log('[FeatureService] fetchAll no productTag, returning local configs', { count: localCount });
471
- return Array.from(this.localConfigs.values());
486
+ return this.localFeatures();
472
487
  }
473
488
  // Fetch from API using ProductBuilder
474
489
  try {
@@ -483,7 +498,7 @@ class FeatureService {
483
498
  catch (error) {
484
499
  // Fall back to local configs if API fails
485
500
  console.log('[FeatureService] fetchAll API failed, falling back to local', { productTag, error: (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error });
486
- return Array.from(this.localConfigs.values());
501
+ return this.localFeatures(productTag);
487
502
  }
488
503
  }
489
504
  /**
@@ -507,7 +522,7 @@ class FeatureService {
507
522
  if (feature) {
508
523
  const config = this.productFeatureToConfig(feature);
509
524
  // Update local cache
510
- this.localConfigs.set(featureTag, config);
525
+ this.localConfigs.set(this.localKey(featureTag, productTag), config);
511
526
  console.log('[FeatureService] fetch SUCCESS from API', { featureTag, productTag, stepsCount: (_a = config.steps) === null || _a === void 0 ? void 0 : _a.length });
512
527
  return config;
513
528
  }
@@ -517,10 +532,10 @@ class FeatureService {
517
532
  catch (error) {
518
533
  // Fall back to local config
519
534
  console.log('[FeatureService] fetch API failed, trying local', { featureTag, productTag, error: (_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : error });
520
- return this.localConfigs.get(featureTag) || null;
535
+ return this.localConfigs.get(this.localKey(featureTag, productTag)) || null;
521
536
  }
522
537
  }
523
- const local = this.localConfigs.get(featureTag) || null;
538
+ const local = this.localConfigs.get(this.localKey(featureTag)) || null;
524
539
  console.log('[FeatureService] fetch from local only', { featureTag, found: !!local });
525
540
  return local;
526
541
  }
@@ -542,10 +557,10 @@ class FeatureService {
542
557
  const builder = await this.getProductBuilder(productTag);
543
558
  await builder.updateFeature(featureTag, updates);
544
559
  // Update local cache
545
- const existing = this.localConfigs.get(featureTag);
560
+ const existing = this.localConfigs.get(this.localKey(featureTag, productTag));
546
561
  if (existing) {
547
562
  const updated = Object.assign(Object.assign({}, existing), { name: updates.name || existing.name, description: (_a = updates.description) !== null && _a !== void 0 ? _a : existing.description, input: (_b = updates.input) !== null && _b !== void 0 ? _b : existing.input, output: (_c = updates.output) !== null && _c !== void 0 ? _c : existing.output, steps: updates.steps || existing.steps, signals: (_d = updates.signals) !== null && _d !== void 0 ? _d : existing.signals, queries: (_e = updates.queries) !== null && _e !== void 0 ? _e : existing.queries, options: (_f = updates.options) !== null && _f !== void 0 ? _f : existing.options, envs: (_g = updates.envs) !== null && _g !== void 0 ? _g : existing.envs });
548
- this.localConfigs.set(featureTag, updated);
563
+ this.localConfigs.set(this.localKey(featureTag, productTag), updated);
549
564
  }
550
565
  console.log('[FeatureService] update SUCCESS', { featureTag, productTag });
551
566
  }
@@ -563,7 +578,7 @@ class FeatureService {
563
578
  const builder = await this.getProductBuilder(productTag);
564
579
  await builder.deleteFeature(featureTag);
565
580
  // Remove from local cache
566
- this.localConfigs.delete(featureTag);
581
+ this.localConfigs.delete(this.localKey(featureTag, productTag));
567
582
  console.log('[FeatureService] delete SUCCESS', { featureTag, productTag });
568
583
  }
569
584
  // ==================== CODE-FIRST API ====================
@@ -679,7 +694,7 @@ class FeatureService {
679
694
  options: schema.options,
680
695
  envs: schema.envs,
681
696
  };
682
- this.localConfigs.set(schema.tag, localConfig);
697
+ this.localConfigs.set(this.localKey(schema.tag, options.product), localConfig);
683
698
  console.log('[FeatureService] define SUCCESS', { tag: options.tag, product: options.product });
684
699
  // Return the defined feature with compile method
685
700
  return {
@@ -699,34 +714,50 @@ class FeatureService {
699
714
  return Promise.all(definitions.map((definition) => new FeatureCompiler(definition).compileAsync()));
700
715
  }
701
716
  /**
702
- * Publish a definition set with read-back verification. If a write fails, newly
703
- * created records are removed and pre-existing records are restored.
717
+ * Publish with content verification and best-effort restoration of existing records.
718
+ * New records are retained. Failures expose per-feature receipts on the error;
719
+ * this is not an atomic transaction, and rejected writes may have persisted.
704
720
  */
705
721
  async publishMany(productTag, definitions, options = {}) {
706
722
  const schemas = await this.compileMany(definitions);
723
+ const receipts = schemas.map(schema => ({
724
+ tag: schema.tag, write: 'not_attempted', verified: false, compensation: 'not_needed',
725
+ }));
707
726
  if (options.dryRun)
708
- return { product: productTag, dry_run: true, features: schemas };
727
+ return { product: productTag, dry_run: true, features: schemas, receipts };
709
728
  const builder = await this.getProductBuilder(productTag);
710
729
  const previous = new Map();
711
730
  const changed = [];
712
731
  try {
713
- for (const schema of schemas) {
732
+ for (const [index, schema] of schemas.entries()) {
733
+ const receipt = receipts[index];
714
734
  const existing = await builder.fetchFeature(schema.tag);
715
735
  previous.set(schema.tag, existing);
736
+ receipt.operation = existing ? 'update' : 'create';
737
+ receipt.write = 'unknown';
738
+ receipt.compensation = existing ? 'not_needed' : 'retained';
739
+ changed.push(schema.tag);
716
740
  if (existing)
717
741
  await builder.updateFeature(schema.tag, schema);
718
742
  else
719
743
  await builder.createFeature(schema, true);
744
+ receipt.write = 'acknowledged';
720
745
  const readBack = await builder.fetchFeature(schema.tag);
721
746
  if (!readBack)
722
747
  throw new Error(`Feature ${schema.tag} is missing after publish`);
723
- changed.push(schema.tag);
748
+ if (this.computeFeatureFingerprint(readBack) !== this.computeFeatureFingerprint(schema)) {
749
+ throw new Error(`Feature ${schema.tag} content differs after publish`);
750
+ }
751
+ receipt.verified = true;
752
+ this.localConfigs.set(this.localKey(schema.tag, productTag), this.productFeatureToConfig(readBack));
724
753
  }
725
- return { product: productTag, dry_run: false, features: schemas };
754
+ return { product: productTag, dry_run: false, features: schemas, receipts };
726
755
  }
727
756
  catch (error) {
728
757
  for (const tag of changed.reverse()) {
729
758
  const old = previous.get(tag);
759
+ const receipt = receipts.find(item => item.tag === tag);
760
+ this.localConfigs.delete(this.localKey(tag, productTag));
730
761
  try {
731
762
  // Restore definitions that existed before this publication attempt.
732
763
  // Never delete a newly created definition as compensation: a
@@ -734,14 +765,22 @@ class FeatureService {
734
765
  // the write is durable. Automatically tombstoning that record turns
735
766
  // an observation failure into data loss and caused entire feature
736
767
  // catalogues to disappear after repeated retries.
737
- if (old)
768
+ if (old) {
769
+ receipt.compensation = 'unknown';
738
770
  await builder.updateFeature(tag, old);
771
+ const restored = await builder.fetchFeature(tag);
772
+ if (!restored || this.computeFeatureFingerprint(restored) !== this.computeFeatureFingerprint(old)) {
773
+ throw new Error(`Feature ${tag} restoration could not be verified`);
774
+ }
775
+ receipt.compensation = 'restored';
776
+ this.localConfigs.set(this.localKey(tag, productTag), this.productFeatureToConfig(restored));
777
+ }
739
778
  }
740
779
  catch (rollbackError) {
741
780
  console.error('[Ductape] Feature batch rollback failed', { product: productTag, tag, rollbackError });
742
781
  }
743
782
  }
744
- throw error;
783
+ throw new FeaturePublicationError(error instanceof Error ? error.message : String(error), productTag, receipts, error);
745
784
  }
746
785
  }
747
786
  // ==================== WORKFLOW EXECUTION ====================
@@ -779,7 +818,7 @@ class FeatureService {
779
818
  // Prefer locally cached feature (from a recent define()) so step conditions and full schema are preserved
780
819
  const builder = await this.getProductBuilder(options.product);
781
820
  console.log('[FeatureService] execute fetching feature', { tag: options.tag, product: options.product });
782
- const localConfig = this.localConfigs.get(options.tag);
821
+ const localConfig = this.localConfigs.get(this.localKey(options.tag, options.product));
783
822
  let feature = localConfig
784
823
  ? Object.assign(Object.assign({}, localConfig), { tag: localConfig.tag, name: localConfig.name, steps: localConfig.steps }) : null;
785
824
  if (feature) {