forge-workflow 0.1.0-beta.6 → 0.1.0-beta.7

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 (156) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/docs/guides/MIGRATION.md +3 -3
  3. package/docs/reference/RELEASE.md +3 -3
  4. package/lib/commands/merge.js +2 -2
  5. package/lib/commands/push.js +13 -4
  6. package/lib/commands/shepherd.js +1 -1
  7. package/lib/commands/validate.js +13 -1
  8. package/lib/kernel/migrations.js +1 -1
  9. package/lib/kernel/sqlite-driver.js +1 -1
  10. package/lib/memory/router.js +1 -1
  11. package/lib/memory/usage-evidence.js +1 -1
  12. package/lib/pr-monitor/flow-monitor.js +2 -2
  13. package/lib/pr-monitor/reconcile-executor.js +1 -1
  14. package/lib/project-memory.js +1 -1
  15. package/lib/validation-receipt.js +190 -0
  16. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  17. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  18. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  19. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  20. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  21. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  22. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  23. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  24. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  25. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  26. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  27. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  28. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  29. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  30. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  31. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  32. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  33. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  34. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  35. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  36. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  37. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  38. package/node_modules/@forge/contracts/index.js +32 -0
  39. package/node_modules/@forge/contracts/package.json +35 -0
  40. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  41. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  42. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  43. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  44. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  45. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  46. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  47. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  48. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  49. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  50. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  51. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  52. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  53. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  54. package/node_modules/@forge/contracts/src/identity.js +42 -0
  55. package/node_modules/@forge/contracts/src/schema.js +72 -0
  56. package/node_modules/@forge/contracts/src/validate.js +305 -0
  57. package/node_modules/@forge/flow/index.js +114 -0
  58. package/node_modules/@forge/flow/package.json +34 -0
  59. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  60. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  61. package/node_modules/@forge/flow/src/executor.js +279 -0
  62. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  63. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  64. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  65. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  66. package/node_modules/@forge/memory/index.js +331 -0
  67. package/node_modules/@forge/memory/package.json +34 -0
  68. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  69. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  70. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  71. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  72. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  73. package/package.json +9 -1
  74. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  75. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  76. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  77. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  78. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  79. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  80. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  81. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  82. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  83. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  84. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  85. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  86. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  87. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  88. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  89. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  90. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  91. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  92. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  93. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  94. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  95. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  96. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  97. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  98. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  99. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  100. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  101. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  102. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  103. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  104. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  105. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  106. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  107. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  108. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  109. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  110. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  111. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  112. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  113. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  114. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  115. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  116. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  117. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  118. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  119. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  120. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  121. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  122. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  123. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  124. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  125. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  126. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  127. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  128. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  129. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  130. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  131. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  132. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  133. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  134. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  135. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  136. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  137. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  138. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  139. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  140. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  141. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  142. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  143. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  144. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  145. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  146. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  147. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  148. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  149. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  150. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  151. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  152. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  153. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  154. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  155. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  156. package/scripts/test.js +1 -1
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const MEMORY_AUTHORITY_METHODS = Object.freeze([
4
+ 'initialize',
5
+ 'runIssueOperation',
6
+ 'runGuardedEvent',
7
+ 'importIssues',
8
+ 'listRecentEvents',
9
+ 'listOpenPrs',
10
+ 'upsertPr',
11
+ 'updatePrVerdict',
12
+ 'retirePr',
13
+ 'listProjectionOutbox',
14
+ 'loadProjectionModel',
15
+ 'markProjectionDelivered',
16
+ 'recordProjectionFailure',
17
+ 'deadLetterProjection',
18
+ ]);
19
+
20
+ // Optional PR lifecycle operations are intentionally separate from the mandatory
21
+ // Memory authority surface. Legacy brokers keep the exact historical provider
22
+ // keys; lifecycle consumers opt into the pair atomically.
23
+ const MEMORY_PR_LIFECYCLE_METHODS = Object.freeze(['recordPrLinkage', 'recordOpenedPrLinkage', 'readTrace']);
24
+
25
+ function assertMemoryAuthorityProvider(provider, label = 'Memory authority provider') {
26
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider)) {
27
+ throw new TypeError(`${label} must be an object`);
28
+ }
29
+ for (const method of MEMORY_AUTHORITY_METHODS) {
30
+ if (typeof provider[method] !== 'function') {
31
+ throw new TypeError(`${label} must implement ${method}()`);
32
+ }
33
+ }
34
+ return provider;
35
+ }
36
+
37
+ function createMemoryAuthorityProvider({ broker } = {}) {
38
+ assertMemoryAuthorityProvider(broker, 'Kernel broker');
39
+ const lifecyclePresence = MEMORY_PR_LIFECYCLE_METHODS.map(method => typeof broker[method] === 'function');
40
+ if (lifecyclePresence.some(Boolean) && !lifecyclePresence.every(Boolean)) {
41
+ throw new TypeError('Kernel broker lifecycle surface must implement recordPrLinkage() and readTrace()');
42
+ }
43
+ const provider = {};
44
+ for (const method of MEMORY_AUTHORITY_METHODS) {
45
+ provider[method] = (...args) => broker[method](...args);
46
+ }
47
+ provider.initialize = async (...args) => {
48
+ const result = await broker.initialize(...args);
49
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
50
+ throw new TypeError('Kernel broker initialize() must return an object');
51
+ }
52
+ return { success: result.success === true };
53
+ };
54
+ if (lifecyclePresence.every(Boolean)) {
55
+ for (const method of MEMORY_PR_LIFECYCLE_METHODS) {
56
+ provider[method] = (...args) => broker[method](...args);
57
+ }
58
+ }
59
+ return Object.freeze(provider);
60
+ }
61
+
62
+ module.exports = {
63
+ MEMORY_AUTHORITY_METHODS,
64
+ MEMORY_PR_LIFECYCLE_METHODS,
65
+ assertMemoryAuthorityProvider,
66
+ createMemoryAuthorityProvider,
67
+ };
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const BACKEND_METHODS = Object.freeze(['add', 'recall', 'search', 'capture', 'digest']);
4
+ const RECEIPT_SCHEMA_ID = 'forge.memory.operation-receipt.v1';
5
+ const RECEIPT_SCHEMA_VERSION = 1;
6
+ const asyncUnsupportedErrors = new WeakSet();
7
+
8
+ function assertBackendName(name) {
9
+ if (typeof name !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(name)) {
10
+ throw new TypeError('memory backend name must use lowercase letters, numbers, and hyphens');
11
+ }
12
+ }
13
+
14
+ function assertBackend(adapter, name = 'memory') {
15
+ if (!adapter || typeof adapter !== 'object' || Array.isArray(adapter)) {
16
+ throw new TypeError(`${name} memory backend must be an object`);
17
+ }
18
+ for (const method of BACKEND_METHODS) {
19
+ if (typeof adapter[method] !== 'function') {
20
+ throw new TypeError(`${name} memory backend must implement ${method}()`);
21
+ }
22
+ }
23
+ }
24
+
25
+ function errorRecord(backend, error) {
26
+ return {
27
+ backend,
28
+ code: asyncUnsupportedErrors.has(error)
29
+ ? 'MEMORY_BACKEND_ASYNC_UNSUPPORTED'
30
+ : 'MEMORY_BACKEND_FAILED',
31
+ };
32
+ }
33
+
34
+ function isThenable(value) {
35
+ return value && (typeof value === 'object' || typeof value === 'function')
36
+ && typeof value.then === 'function';
37
+ }
38
+
39
+ function requireSynchronous(value, backend) {
40
+ if (!isThenable(value)) return value;
41
+ value.then(() => {}, () => {});
42
+ const error = new TypeError(`${backend} memory backend methods must be synchronous`);
43
+ asyncUnsupportedErrors.add(error);
44
+ throw error;
45
+ }
46
+
47
+ function deepFreeze(value, seen = new WeakSet()) {
48
+ if (!value || typeof value !== 'object' || seen.has(value)) return value;
49
+ seen.add(value);
50
+ for (const child of Object.values(value)) deepFreeze(child, seen);
51
+ return Object.freeze(value);
52
+ }
53
+
54
+ function detached(value, { freeze = false } = {}) {
55
+ const copy = structuredClone(value);
56
+ return freeze ? deepFreeze(copy) : copy;
57
+ }
58
+
59
+ function identity(value) {
60
+ if (value && typeof value === 'object') {
61
+ if (typeof value.id === 'string') return `id:${value.id}`;
62
+ if (typeof value.key === 'string') return `key:${value.key}`;
63
+ }
64
+ return `value:${JSON.stringify(value)}`;
65
+ }
66
+
67
+ function enrichLocalValue(localValue, enrichedValue, operation) {
68
+ if (!['recall', 'search', 'digest'].includes(operation)) return localValue;
69
+ if (!Array.isArray(localValue) || !Array.isArray(enrichedValue)) return localValue;
70
+ const seen = new Set(localValue.map(identity));
71
+ return [...localValue, ...enrichedValue.filter(item => {
72
+ const key = identity(item);
73
+ if (seen.has(key)) return false;
74
+ seen.add(key);
75
+ return true;
76
+ })];
77
+ }
78
+
79
+ function createMemoryBackendRegistry({ local, selected = 'local' } = {}) {
80
+ assertBackend(local, 'local');
81
+ const adapters = new Map([['local', local]]);
82
+ let selectedBackend = 'local';
83
+
84
+ function register(name, adapter) {
85
+ assertBackendName(name);
86
+ if (name === 'local') throw new Error('the local memory floor cannot be replaced');
87
+ if (adapters.has(name)) throw new Error(`memory backend "${name}" is already registered`);
88
+ assertBackend(adapter, name);
89
+ adapters.set(name, adapter);
90
+ return api;
91
+ }
92
+
93
+ function unregister(name) {
94
+ if (name === 'local') throw new Error('the local memory floor cannot be removed');
95
+ adapters.delete(name);
96
+ if (selectedBackend === name) selectedBackend = 'local';
97
+ return api;
98
+ }
99
+
100
+ function select(name) {
101
+ assertBackendName(name);
102
+ if (!adapters.has(name)) throw new Error(`memory backend "${name}" is not registered`);
103
+ selectedBackend = name;
104
+ return api;
105
+ }
106
+
107
+ function execute(operation, args) {
108
+ const localValue = requireSynchronous(adapters.get('local')[operation](...args), 'local');
109
+ const backends = [{ backend: 'local', status: 'ok' }];
110
+ const errors = [];
111
+ let value = localValue;
112
+
113
+ if (selectedBackend !== 'local') {
114
+ try {
115
+ const localSnapshot = detached(localValue);
116
+ const enrichedArgs = detached(args, { freeze: true });
117
+ value = localSnapshot;
118
+ const enrichedValue = requireSynchronous(
119
+ adapters.get(selectedBackend)[operation](...enrichedArgs),
120
+ selectedBackend,
121
+ );
122
+ value = enrichLocalValue(localSnapshot, enrichedValue, operation);
123
+ backends.push({ backend: selectedBackend, status: 'ok' });
124
+ } catch (error) {
125
+ backends.push({ backend: selectedBackend, status: 'failed' });
126
+ errors.push(errorRecord(selectedBackend, error));
127
+ }
128
+ }
129
+
130
+ return {
131
+ value,
132
+ receipt: {
133
+ schema_id: RECEIPT_SCHEMA_ID,
134
+ schema_version: RECEIPT_SCHEMA_VERSION,
135
+ operation,
136
+ selected_backend: selectedBackend,
137
+ local_floor: 'local',
138
+ status: errors.length === 0 ? 'ok' : 'degraded',
139
+ capabilities: [...BACKEND_METHODS].sort(),
140
+ backends,
141
+ errors,
142
+ },
143
+ };
144
+ }
145
+
146
+ const api = {
147
+ register,
148
+ unregister,
149
+ select,
150
+ selected: () => selectedBackend,
151
+ names: () => [...adapters.keys()].sort((a, b) => (a === 'local' ? -1 : b === 'local' ? 1 : a.localeCompare(b))),
152
+ };
153
+ for (const operation of BACKEND_METHODS) {
154
+ api[operation] = (...args) => execute(operation, args);
155
+ }
156
+ select(selected);
157
+ return Object.freeze(api);
158
+ }
159
+
160
+ module.exports = {
161
+ BACKEND_METHODS,
162
+ RECEIPT_SCHEMA_ID,
163
+ RECEIPT_SCHEMA_VERSION,
164
+ assertBackend,
165
+ createMemoryBackendRegistry,
166
+ };
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ const { createHash, randomUUID: defaultRandomUUID } = require('node:crypto');
4
+ const {
5
+ canonicalize,
6
+ computeContentHash,
7
+ semanticIdentity,
8
+ validateContractStructure,
9
+ } = require('@forge/contracts');
10
+
11
+ const FEEDBACK_SCHEMA_ID = 'forge.memory.feedback-report.v1';
12
+ const ASSIGNED_SECRET = /((?:api[_-]?key|token|secret|password)\s*[:=]\s*)\S{8,}/gi;
13
+ const BARE_SECRET = /(?:gh[pousr]_[A-Za-z0-9]{20,}|sk_(?:live|test)_[A-Za-z0-9]{16,}|sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16})/gi;
14
+ const USER_PATH = /(?:[A-Za-z]:\\Users\\[^\\\s]+\\|\/(?:Users|home)\/[^/\s]+\/)/gi;
15
+
16
+ class FeedbackIntakeError extends Error {
17
+ constructor(code, message) {
18
+ super(message);
19
+ this.name = 'FeedbackIntakeError';
20
+ this.code = code;
21
+ }
22
+ }
23
+
24
+ function requiredString(value, name) {
25
+ if (typeof value !== 'string' || value.length === 0) {
26
+ throw new TypeError(`${name} must be a non-empty string`);
27
+ }
28
+ return value;
29
+ }
30
+
31
+ function redactString(value) {
32
+ return value
33
+ .replace(USER_PATH, '<user-path>/')
34
+ .replace(ASSIGNED_SECRET, '[REDACTED]')
35
+ .replace(BARE_SECRET, '[REDACTED]');
36
+ }
37
+
38
+ function redactValue(value) {
39
+ if (typeof value === 'string') return redactString(value);
40
+ if (Array.isArray(value)) return value.map(redactValue);
41
+ if (!value || typeof value !== 'object') return value;
42
+ const output = {};
43
+ for (const [key, child] of Object.entries(value)) {
44
+ const safeKey = redactString(key);
45
+ if (Object.hasOwn(output, safeKey)) {
46
+ throw new TypeError('redaction produced duplicate returnChannel keys');
47
+ }
48
+ output[safeKey] = redactValue(child);
49
+ }
50
+ return output;
51
+ }
52
+
53
+ function sha256(value) {
54
+ return createHash('sha256').update(canonicalize(value), 'utf8').digest('hex');
55
+ }
56
+
57
+ function assertFeedbackReport(report) {
58
+ const validation = validateContractStructure(report);
59
+ if (report?.schema_id !== FEEDBACK_SCHEMA_ID || !validation.ok) {
60
+ const codes = validation.errors.map(item => item.code).join(', ') || 'WRONG_SCHEMA';
61
+ throw new FeedbackIntakeError('FEEDBACK_REPORT_INVALID', `FeedbackReport is invalid: ${codes}`);
62
+ }
63
+ return report;
64
+ }
65
+
66
+ function createFeedbackReport(input = {}, options = {}) {
67
+ const randomUUID = options.randomUUID || defaultRandomUUID;
68
+ const now = options.now || (() => new Date().toISOString());
69
+ const reportId = randomUUID();
70
+ const reproductionSteps = input.reproductionSteps;
71
+ if (!Array.isArray(reproductionSteps) || reproductionSteps.length === 0) {
72
+ throw new TypeError('reproductionSteps must be a non-empty array');
73
+ }
74
+ const redactedSteps = reproductionSteps.map((step, index) =>
75
+ redactString(requiredString(step, `reproductionSteps[${index}]`)));
76
+ const stableErrorCode = redactString(requiredString(input.stableErrorCode, 'stableErrorCode'));
77
+ const affectedCapability = redactString(requiredString(input.affectedCapability, 'affectedCapability'));
78
+ const expectedClassification = redactString(requiredString(input.expectedClassification, 'expectedClassification'));
79
+ const actualClassification = redactString(requiredString(input.actualClassification, 'actualClassification'));
80
+ const productVersion = redactString(requiredString(input.productVersion, 'productVersion'));
81
+ const redactionPolicyRevision = redactString(requiredString(input.redactionPolicyRevision, 'redactionPolicyRevision'));
82
+ const consentEventId = redactString(requiredString(input.consentEventId, 'consentEventId'));
83
+ const occurrenceCount = input.occurrenceCount ?? 1;
84
+ if (!Number.isInteger(occurrenceCount) || occurrenceCount < 1) {
85
+ throw new TypeError('occurrenceCount must be a positive integer');
86
+ }
87
+
88
+ const payload = {
89
+ report_id: reportId,
90
+ product_version: productVersion,
91
+ contract_version: 1,
92
+ stable_error_code: stableErrorCode,
93
+ affected_capability: affectedCapability,
94
+ redacted_reproduction_steps: redactedSteps,
95
+ expected_classification: expectedClassification,
96
+ actual_classification: actualClassification,
97
+ occurrence_count: occurrenceCount,
98
+ content_fingerprint: sha256({
99
+ stable_error_code: stableErrorCode,
100
+ affected_capability: affectedCapability,
101
+ redacted_reproduction_steps: redactedSteps,
102
+ expected_classification: expectedClassification,
103
+ actual_classification: actualClassification,
104
+ }),
105
+ consent_event_id: consentEventId,
106
+ redaction_policy_revision: redactionPolicyRevision,
107
+ };
108
+ if (input.proposedFix !== undefined) {
109
+ payload.proposed_fix = redactString(requiredString(input.proposedFix, 'proposedFix'));
110
+ }
111
+ if (input.returnChannel !== undefined) payload.return_channel = redactValue(input.returnChannel);
112
+
113
+ const report = {
114
+ schema_id: FEEDBACK_SCHEMA_ID,
115
+ schema_version: 1,
116
+ object_id: reportId,
117
+ created_at: now(),
118
+ producer: {
119
+ product_id: 'forge-memory',
120
+ product_version: productVersion,
121
+ // A per-report nonce prevents anonymous reports from becoming a cross-report tracking key.
122
+ instance_id: reportId,
123
+ },
124
+ capabilities_used: [],
125
+ provenance: {
126
+ source_kind: 'feedback',
127
+ actor_class: 'anonymous-user',
128
+ actor_id: reportId,
129
+ },
130
+ payload,
131
+ extensions: {},
132
+ };
133
+ report.content_hash = computeContentHash(report);
134
+ return assertFeedbackReport(report);
135
+ }
136
+
137
+ function assertConsent(report, consent) {
138
+ if (!consent || consent.approved !== true) return false;
139
+ if (consent.eventId !== report.payload.consent_event_id
140
+ || consent.redactionPolicyRevision !== report.payload.redaction_policy_revision
141
+ || consent.reportId !== report.payload.report_id
142
+ || consent.contentHash !== report.content_hash) {
143
+ throw new FeedbackIntakeError(
144
+ 'FEEDBACK_CONSENT_MISMATCH',
145
+ 'Feedback consent must match this report and redaction-policy revision',
146
+ );
147
+ }
148
+ return true;
149
+ }
150
+
151
+ function createFeedbackIntake({ acceptFeedback, deliverFeedback, deliveryTimeoutMs = 10_000 } = {}) {
152
+ if (typeof acceptFeedback !== 'function') {
153
+ throw new TypeError('acceptFeedback must be an atomic durable acceptance function');
154
+ }
155
+ if (deliverFeedback !== undefined && typeof deliverFeedback !== 'function') {
156
+ throw new TypeError('deliverFeedback must be a function when provided');
157
+ }
158
+ if (!Number.isInteger(deliveryTimeoutMs) || deliveryTimeoutMs < 1) {
159
+ throw new TypeError('deliveryTimeoutMs must be a positive integer');
160
+ }
161
+
162
+ function preview(report) {
163
+ assertFeedbackReport(report);
164
+ return { status: 'pending-consent', report: structuredClone(report) };
165
+ }
166
+
167
+ async function submit(report, consent) {
168
+ assertFeedbackReport(report);
169
+ if (!assertConsent(report, consent)) {
170
+ return {
171
+ status: consent?.approved === false ? 'declined' : 'pending-consent',
172
+ accepted: false,
173
+ delivered: false,
174
+ };
175
+ }
176
+
177
+ const identity = semanticIdentity(report);
178
+ const result = await acceptFeedback({
179
+ identity,
180
+ content_hash: report.content_hash,
181
+ report: structuredClone(report),
182
+ });
183
+ if (result?.status === 'identity-conflict') {
184
+ throw new FeedbackIntakeError(
185
+ 'FEEDBACK_IDENTITY_CONFLICT',
186
+ 'Feedback report identity was already accepted with different content',
187
+ );
188
+ }
189
+ if (result?.status === 'retry-identical') {
190
+ return { status: 'retry-identical', accepted: false, delivered: false, receipt: result.receipt };
191
+ }
192
+ if (result?.status !== 'accepted') {
193
+ throw new FeedbackIntakeError('FEEDBACK_ACCEPTANCE_INVALID', 'Feedback authority returned an invalid result');
194
+ }
195
+ if (!deliverFeedback) {
196
+ return { status: 'accepted-local', accepted: true, delivered: false, receipt: result.receipt };
197
+ }
198
+ const controller = new AbortController();
199
+ let timeoutId;
200
+ const timeout = new Promise((_, reject) => {
201
+ timeoutId = setTimeout(() => {
202
+ controller.abort();
203
+ reject(new FeedbackIntakeError('FEEDBACK_DELIVERY_TIMEOUT', 'Feedback delivery timed out'));
204
+ }, deliveryTimeoutMs);
205
+ });
206
+ try {
207
+ const delivery = await Promise.race([
208
+ Promise.resolve().then(() => deliverFeedback(structuredClone(report), {
209
+ receipt: result.receipt,
210
+ signal: controller.signal,
211
+ })),
212
+ timeout,
213
+ ]);
214
+ return { status: 'accepted', accepted: true, delivered: true, receipt: result.receipt, delivery };
215
+ } catch (error) {
216
+ const code = error?.code === 'FEEDBACK_DELIVERY_TIMEOUT'
217
+ ? 'FEEDBACK_DELIVERY_TIMEOUT'
218
+ : 'FEEDBACK_DELIVERY_FAILED';
219
+ return {
220
+ status: 'accepted-local',
221
+ accepted: true,
222
+ delivered: false,
223
+ receipt: result.receipt,
224
+ delivery_error: {
225
+ code,
226
+ reason: redactString(String(error?.message || 'unknown delivery failure')).slice(0, 200),
227
+ },
228
+ };
229
+ } finally {
230
+ clearTimeout(timeoutId);
231
+ }
232
+ }
233
+
234
+ return Object.freeze({ preview, submit });
235
+ }
236
+
237
+ module.exports = {
238
+ FEEDBACK_SCHEMA_ID,
239
+ FeedbackIntakeError,
240
+ createFeedbackIntake,
241
+ createFeedbackReport,
242
+ redactString,
243
+ };