@ai-sdlc/orchestrator 0.9.0 → 0.13.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 (172) hide show
  1. package/dist/__test-helpers/git-env.d.ts +40 -0
  2. package/dist/__test-helpers/git-env.js +73 -0
  3. package/dist/adapters.d.ts +42 -3
  4. package/dist/adapters.js +133 -3
  5. package/dist/admission-composite.d.ts +134 -1
  6. package/dist/admission-composite.js +170 -6
  7. package/dist/admission-enrichment.d.ts +12 -3
  8. package/dist/admission-enrichment.js +71 -10
  9. package/dist/admission-hc.d.ts +20 -1
  10. package/dist/admission-hc.js +12 -1
  11. package/dist/admission-score.d.ts +44 -0
  12. package/dist/admission-score.js +11 -2
  13. package/dist/backlog-adapter.d.ts +45 -0
  14. package/dist/backlog-adapter.js +145 -1
  15. package/dist/calibration.d.ts +81 -0
  16. package/dist/calibration.js +76 -0
  17. package/dist/cli/commands/git-remote.d.ts +25 -0
  18. package/dist/cli/commands/git-remote.js +80 -1
  19. package/dist/cli/commands/init-features.d.ts +507 -0
  20. package/dist/cli/commands/init-features.js +1100 -0
  21. package/dist/cli/commands/init-templates.d.ts +301 -0
  22. package/dist/cli/commands/init-templates.js +1341 -0
  23. package/dist/cli/commands/init.d.ts +67 -0
  24. package/dist/cli/commands/init.js +301 -2
  25. package/dist/cli/commands/run.js +9 -1
  26. package/dist/cli/index.d.ts +11 -0
  27. package/dist/cli/index.js +42 -8
  28. package/dist/compliance/composer.d.ts +79 -0
  29. package/dist/compliance/composer.js +258 -0
  30. package/dist/compliance/errors.d.ts +64 -0
  31. package/dist/compliance/errors.js +85 -0
  32. package/dist/compliance/loader.d.ts +52 -0
  33. package/dist/compliance/loader.js +124 -0
  34. package/dist/compliance/types.d.ts +184 -0
  35. package/dist/compliance/types.js +41 -0
  36. package/dist/compliance-clearance.d.ts +269 -0
  37. package/dist/compliance-clearance.js +269 -0
  38. package/dist/config.js +17 -0
  39. package/dist/cost-tracker.d.ts +22 -0
  40. package/dist/cost-tracker.js +41 -0
  41. package/dist/database/adapters/external.js +5 -1
  42. package/dist/design-authority.d.ts +13 -0
  43. package/dist/design-authority.js +13 -0
  44. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  45. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  46. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  47. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  48. package/dist/embedding/cross-provider.d.ts +78 -0
  49. package/dist/embedding/cross-provider.js +75 -0
  50. package/dist/embedding/deprecation.d.ts +151 -0
  51. package/dist/embedding/deprecation.js +229 -0
  52. package/dist/embedding/errors.d.ts +90 -0
  53. package/dist/embedding/errors.js +150 -0
  54. package/dist/embedding/index.d.ts +29 -0
  55. package/dist/embedding/index.js +24 -0
  56. package/dist/embedding/pipeline-load.d.ts +146 -0
  57. package/dist/embedding/pipeline-load.js +178 -0
  58. package/dist/embedding/registry.d.ts +45 -0
  59. package/dist/embedding/registry.js +61 -0
  60. package/dist/embedding/stale-vector.d.ts +110 -0
  61. package/dist/embedding/stale-vector.js +92 -0
  62. package/dist/embedding/storage/index.d.ts +51 -0
  63. package/dist/embedding/storage/index.js +43 -0
  64. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  65. package/dist/embedding/storage/jsonl-backend.js +332 -0
  66. package/dist/embedding/storage/types.d.ts +135 -0
  67. package/dist/embedding/storage/types.js +13 -0
  68. package/dist/embedding/types.d.ts +180 -0
  69. package/dist/embedding/types.js +10 -0
  70. package/dist/execute.d.ts +29 -2
  71. package/dist/execute.js +179 -35
  72. package/dist/index.d.ts +14 -2
  73. package/dist/index.js +19 -2
  74. package/dist/journey/inheritance-validator.d.ts +396 -0
  75. package/dist/journey/inheritance-validator.js +370 -0
  76. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  77. package/dist/journey/state-id-drift-rule.js +245 -0
  78. package/dist/journey-sa2-router.d.ts +395 -0
  79. package/dist/journey-sa2-router.js +308 -0
  80. package/dist/models/classifier.d.ts +3 -1
  81. package/dist/models/classifier.js +61 -5
  82. package/dist/pillar-breakdown.d.ts +23 -1
  83. package/dist/pillar-breakdown.js +9 -0
  84. package/dist/runners/review-agent.js +6 -2
  85. package/dist/runners/runner-registry.d.ts +36 -0
  86. package/dist/runners/runner-registry.js +90 -0
  87. package/dist/runtime/attestations.d.ts +663 -11
  88. package/dist/runtime/attestations.js +971 -24
  89. package/dist/runtime/index.d.ts +1 -1
  90. package/dist/runtime/index.js +1 -1
  91. package/dist/runtime/parallelism-flag.d.ts +21 -3
  92. package/dist/runtime/parallelism-flag.js +27 -6
  93. package/dist/runtime/port-allocator.d.ts +11 -0
  94. package/dist/runtime/port-allocator.js +30 -0
  95. package/dist/sa-scoring/layer3-llm.js +6 -1
  96. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  97. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  98. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  99. package/dist/sa-scoring/revision-proposal.js +417 -0
  100. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  101. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  102. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  103. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  104. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  105. package/dist/signal-ingestion/adapters/manual.js +112 -0
  106. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  107. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  108. package/dist/signal-ingestion/classifier.d.ts +205 -0
  109. package/dist/signal-ingestion/classifier.js +494 -0
  110. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  111. package/dist/signal-ingestion/clustering-types.js +14 -0
  112. package/dist/signal-ingestion/clustering.d.ts +200 -0
  113. package/dist/signal-ingestion/clustering.js +413 -0
  114. package/dist/signal-ingestion/config.d.ts +351 -0
  115. package/dist/signal-ingestion/config.js +587 -0
  116. package/dist/signal-ingestion/d1.d.ts +252 -0
  117. package/dist/signal-ingestion/d1.js +235 -0
  118. package/dist/signal-ingestion/errors.d.ts +73 -0
  119. package/dist/signal-ingestion/errors.js +108 -0
  120. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  121. package/dist/signal-ingestion/governance-events.js +189 -0
  122. package/dist/signal-ingestion/index.d.ts +35 -0
  123. package/dist/signal-ingestion/index.js +53 -0
  124. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  125. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  126. package/dist/signal-ingestion/registry.d.ts +40 -0
  127. package/dist/signal-ingestion/registry.js +137 -0
  128. package/dist/signal-ingestion/residency.d.ts +227 -0
  129. package/dist/signal-ingestion/residency.js +238 -0
  130. package/dist/signal-ingestion/significance.d.ts +554 -0
  131. package/dist/signal-ingestion/significance.js +555 -0
  132. package/dist/signal-ingestion/types.d.ts +191 -0
  133. package/dist/signal-ingestion/types.js +8 -0
  134. package/dist/substrate/drift-composition.d.ts +270 -0
  135. package/dist/substrate/drift-composition.js +306 -0
  136. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  137. package/dist/substrate/drift-tui-surface.js +102 -0
  138. package/dist/substrate/identity-class.d.ts +176 -0
  139. package/dist/substrate/identity-class.js +201 -0
  140. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  141. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  142. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  143. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  144. package/dist/tessellation/rule-registry.d.ts +269 -0
  145. package/dist/tessellation/rule-registry.js +92 -0
  146. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  147. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  148. package/dist/tessellation-admission.d.ts +162 -0
  149. package/dist/tessellation-admission.js +146 -0
  150. package/dist/tessellation-drift.d.ts +246 -0
  151. package/dist/tessellation-drift.js +250 -0
  152. package/dist/validate-config.js +13 -0
  153. package/dist/validate-issue.js +2 -2
  154. package/dist/variant/cardinality-activation.d.ts +126 -0
  155. package/dist/variant/cardinality-activation.js +101 -0
  156. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  157. package/dist/variant/deprecation-lifecycle.js +208 -0
  158. package/dist/variant/drift-extension.d.ts +136 -0
  159. package/dist/variant/drift-extension.js +164 -0
  160. package/dist/variant/engineering-review.d.ts +185 -0
  161. package/dist/variant/engineering-review.js +142 -0
  162. package/dist/variant/index.d.ts +32 -0
  163. package/dist/variant/index.js +32 -0
  164. package/dist/variant/inheritance-validator.d.ts +165 -0
  165. package/dist/variant/inheritance-validator.js +139 -0
  166. package/dist/variant/internal-adopter/index.d.ts +11 -0
  167. package/dist/variant/internal-adopter/index.js +10 -0
  168. package/dist/variant/internal-adopter/products.d.ts +156 -0
  169. package/dist/variant/internal-adopter/products.js +366 -0
  170. package/dist/variant-admission.d.ts +316 -0
  171. package/dist/variant-admission.js +247 -0
  172. package/package.json +11 -8
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Signal-ingestion governance event logger (RFC-0030 §11 / AISDLC-348).
3
+ *
4
+ * Per the RFC §11 closing note: "Configuration changes require Product
5
+ * Lead approval (logged as governance events; not DID changes but
6
+ * governance-relevant)." This module ships the audit trail.
7
+ *
8
+ * Trigger surface:
9
+ * - `loadSignalIngestionConfigWithGovernance()` calls
10
+ * `loadSignalIngestionConfig()` then diffs the result against
11
+ * `DEFAULT_SIGNAL_INGESTION_CONFIG`. When the diff is non-empty AND
12
+ * a `previousConfigSnapshot` is supplied (operators tracking version
13
+ * boundaries), it appends one `SignalIngestionConfigChanged` JSONL
14
+ * line to the orchestrator events stream.
15
+ * - Pure callers that just want the diff without the I/O can use
16
+ * `computeConfigDiff()` directly.
17
+ *
18
+ * Path convention: events land at
19
+ * `<artifactsDir>/_orchestrator/events-YYYY-MM-DD.jsonl`, the same
20
+ * date-rotated file pipeline-cli's orchestrator writes to (RFC-0015
21
+ * Phase 4 / AISDLC-169.4). Keeping one file means
22
+ * `cli-status --orchestrator` + the TUI events tail surface signal-
23
+ * ingestion governance events alongside dispatch / completion events
24
+ * without per-RFC observability silos.
25
+ *
26
+ * Why a separate writer instead of importing pipeline-cli's `writeEvent()`:
27
+ * `orchestrator/` does not depend on `pipeline-cli/` (the dependency
28
+ * graph runs the other way), so a direct import would invert the layer
29
+ * boundary. The writer here is small enough (`appendFileSync` + mkdir +
30
+ * date-suffix) that duplication is the right trade vs. a circular dep.
31
+ *
32
+ * Best-effort like the orchestrator's `writeEvent()`: never throws, returns
33
+ * a boolean for test observability.
34
+ */
35
+ import { type LoadSignalIngestionConfigOptions, type SignalIngestionConfig, type SignalIngestionConfigDeprecatedFieldDecision } from './config.js';
36
+ /**
37
+ * One field that drifted between two `SignalIngestionConfig` snapshots.
38
+ *
39
+ * `path` uses dot notation (e.g. `tierMultipliers.enterprise`,
40
+ * `clustering.algorithm`). `previous` is the value in the baseline /
41
+ * previous snapshot; `current` is the value in the freshly-loaded config.
42
+ *
43
+ * Array fields (`adapters`, `acceptedLanguages`) are compared as ordered
44
+ * lists — order changes count as drift because the registry honours
45
+ * insertion order for tiebreaks.
46
+ */
47
+ export interface SignalIngestionConfigChange {
48
+ path: string;
49
+ previous: unknown;
50
+ current: unknown;
51
+ }
52
+ /**
53
+ * Result of `computeConfigDiff(previous, current)`. `changed` is true when
54
+ * at least one field drifted. `changes` lists every drifted field; empty
55
+ * when `changed === false`.
56
+ */
57
+ export interface SignalIngestionConfigDiff {
58
+ changed: boolean;
59
+ changes: SignalIngestionConfigChange[];
60
+ }
61
+ /**
62
+ * Compute the field-level diff between two `SignalIngestionConfig`
63
+ * snapshots. Pure / no I/O; safe to call from any context.
64
+ *
65
+ * The diff is deterministic: changes are sorted by `path` lexicographic
66
+ * so the same drift always produces the same `changes` array (audit
67
+ * stability + diff stability across operators).
68
+ */
69
+ export declare function computeConfigDiff(previous: SignalIngestionConfig, current: SignalIngestionConfig): SignalIngestionConfigDiff;
70
+ /**
71
+ * One JSONL line on the orchestrator events stream representing a
72
+ * detected configuration change. Mirrors the discriminator pattern from
73
+ * `pipeline-cli/src/orchestrator/events.ts`: every event carries
74
+ * `{ts, type, ...}`. The `type` value `SignalIngestionConfigChanged` is
75
+ * stable; downstream consumers (`cli-status --orchestrator`, the TUI
76
+ * events pane, Slack push) filter on it without coupling to per-field
77
+ * shape.
78
+ */
79
+ export interface SignalIngestionConfigChangedEvent {
80
+ /** ISO-8601 timestamp set by the writer at append time. */
81
+ ts: string;
82
+ /** Discriminator — stable across the soak window + post-promotion. */
83
+ type: 'SignalIngestionConfigChanged';
84
+ /** Absolute or repo-relative path to the YAML file loaded. */
85
+ configPath: string;
86
+ /** Field-level changes detected vs the previous snapshot. */
87
+ changes: SignalIngestionConfigChange[];
88
+ /**
89
+ * When the previous snapshot was the framework default vs. an actual
90
+ * previous load. Lets dashboards distinguish "operator opted in" (first
91
+ * non-default load) from "operator tuned the config" (delta between
92
+ * two non-default loads).
93
+ */
94
+ comparedAgainst: 'defaults' | 'previous-load';
95
+ }
96
+ export interface WriteConfigChangeEventOpts {
97
+ /**
98
+ * Override the artifacts directory. Falls back to env `ARTIFACTS_DIR`
99
+ * then `<cwd>/artifacts`, matching pipeline-cli's `writeEvent()` so
100
+ * both writers land in the same date-rotated file.
101
+ */
102
+ artifactsDir?: string;
103
+ /** Override `Date.now()` for the rotation suffix + the event `ts`. */
104
+ now?: () => Date;
105
+ /**
106
+ * Optional warn-sink for best-effort write failures. The orchestrator
107
+ * passes its CLI logger; tests pass a capturing stub.
108
+ */
109
+ warn?: (msg: string) => void;
110
+ }
111
+ /**
112
+ * Append a `SignalIngestionConfigChanged` event to the orchestrator's
113
+ * date-rotated events file. Best-effort: returns `false` on write failure
114
+ * (logged via `warn`) instead of throwing.
115
+ */
116
+ export declare function writeSignalIngestionConfigChangedEvent(event: SignalIngestionConfigChangedEvent, opts?: WriteConfigChangeEventOpts): boolean;
117
+ /**
118
+ * Resolve the on-disk path for the date-rotated events file. Mirrors
119
+ * `pipeline-cli/src/orchestrator/events.ts#eventsFilePath()` so the two
120
+ * writers append to the same file. Exported so tests + cli-status can
121
+ * derive the same path without duplicating the rotation logic.
122
+ */
123
+ export declare function eventsFilePath(artifactsDir: string, date?: Date): string;
124
+ export interface LoadConfigWithGovernanceOptions extends LoadSignalIngestionConfigOptions {
125
+ /**
126
+ * Previous-load snapshot to compare against. When omitted, the diff is
127
+ * computed against `DEFAULT_SIGNAL_INGESTION_CONFIG` and the event's
128
+ * `comparedAgainst` field is set to `'defaults'`.
129
+ *
130
+ * Operators tracking version-to-version drift (e.g. a long-running
131
+ * orchestrator that reloads the config on a tick) pass the previous
132
+ * resolved config here; the event then carries `comparedAgainst:
133
+ * 'previous-load'` so dashboards can distinguish "first load on this
134
+ * project" from "operator tuned the knobs mid-run".
135
+ */
136
+ previousConfigSnapshot?: SignalIngestionConfig;
137
+ /** Override artifacts dir + clock — passed through to the event writer. */
138
+ artifactsDir?: string;
139
+ now?: () => Date;
140
+ warn?: (msg: string) => void;
141
+ /**
142
+ * When true, suppress the events.jsonl append (still computes the diff
143
+ * and returns it). Tests use this to assert the diff shape without
144
+ * touching the filesystem; production callers leave it false.
145
+ */
146
+ skipEventEmission?: boolean;
147
+ }
148
+ export interface LoadConfigWithGovernanceResult {
149
+ config: SignalIngestionConfig;
150
+ diff: SignalIngestionConfigDiff;
151
+ /** True when an event was successfully written to events.jsonl. */
152
+ eventWritten: boolean;
153
+ /** Absolute path the loader used (echoed for downstream logging). */
154
+ configPath: string;
155
+ /**
156
+ * Deprecation Decisions emitted during config load — e.g. legacy
157
+ * `sourceBaselineDriftMultiplier` translated to `zScoreThreshold`. Empty
158
+ * array when no legacy keys were present. Callers should pipe each into
159
+ * `cli-decisions add` so the operator sees the soft-deprecation window
160
+ * status. AISDLC-433 follow-up: governance loader now routes through
161
+ * `loadSignalIngestionConfigWithDeprecations` so the audit trail does
162
+ * NOT silently drop legacy YAML keys.
163
+ */
164
+ deprecations: SignalIngestionConfigDeprecatedFieldDecision[];
165
+ }
166
+ /**
167
+ * Load the signal-ingestion config and emit a `SignalIngestionConfigChanged`
168
+ * event when the loaded config differs from the comparison baseline
169
+ * (defaults, or `previousConfigSnapshot` when supplied).
170
+ *
171
+ * Returns `{config, diff, eventWritten, configPath}`. `eventWritten` is
172
+ * `false` when (a) there was no diff to report, (b) the caller passed
173
+ * `skipEventEmission: true`, or (c) the best-effort write threw.
174
+ *
175
+ * This is the canonical entry point for orchestrator surfaces that want
176
+ * the audit trail. Pure callers that just need the resolved config can
177
+ * keep using `loadSignalIngestionConfig()` directly — they bypass the
178
+ * governance layer entirely.
179
+ */
180
+ export declare function loadSignalIngestionConfigWithGovernance(options?: LoadConfigWithGovernanceOptions): LoadConfigWithGovernanceResult;
181
+ //# sourceMappingURL=governance-events.d.ts.map
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Signal-ingestion governance event logger (RFC-0030 §11 / AISDLC-348).
3
+ *
4
+ * Per the RFC §11 closing note: "Configuration changes require Product
5
+ * Lead approval (logged as governance events; not DID changes but
6
+ * governance-relevant)." This module ships the audit trail.
7
+ *
8
+ * Trigger surface:
9
+ * - `loadSignalIngestionConfigWithGovernance()` calls
10
+ * `loadSignalIngestionConfig()` then diffs the result against
11
+ * `DEFAULT_SIGNAL_INGESTION_CONFIG`. When the diff is non-empty AND
12
+ * a `previousConfigSnapshot` is supplied (operators tracking version
13
+ * boundaries), it appends one `SignalIngestionConfigChanged` JSONL
14
+ * line to the orchestrator events stream.
15
+ * - Pure callers that just want the diff without the I/O can use
16
+ * `computeConfigDiff()` directly.
17
+ *
18
+ * Path convention: events land at
19
+ * `<artifactsDir>/_orchestrator/events-YYYY-MM-DD.jsonl`, the same
20
+ * date-rotated file pipeline-cli's orchestrator writes to (RFC-0015
21
+ * Phase 4 / AISDLC-169.4). Keeping one file means
22
+ * `cli-status --orchestrator` + the TUI events tail surface signal-
23
+ * ingestion governance events alongside dispatch / completion events
24
+ * without per-RFC observability silos.
25
+ *
26
+ * Why a separate writer instead of importing pipeline-cli's `writeEvent()`:
27
+ * `orchestrator/` does not depend on `pipeline-cli/` (the dependency
28
+ * graph runs the other way), so a direct import would invert the layer
29
+ * boundary. The writer here is small enough (`appendFileSync` + mkdir +
30
+ * date-suffix) that duplication is the right trade vs. a circular dep.
31
+ *
32
+ * Best-effort like the orchestrator's `writeEvent()`: never throws, returns
33
+ * a boolean for test observability.
34
+ */
35
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
36
+ import { dirname, join } from 'node:path';
37
+ import { DEFAULT_SIGNAL_INGESTION_CONFIG, loadSignalIngestionConfigWithDeprecations, } from './config.js';
38
+ // ── Diff function ──────────────────────────────────────────────────────
39
+ /**
40
+ * Compute the field-level diff between two `SignalIngestionConfig`
41
+ * snapshots. Pure / no I/O; safe to call from any context.
42
+ *
43
+ * The diff is deterministic: changes are sorted by `path` lexicographic
44
+ * so the same drift always produces the same `changes` array (audit
45
+ * stability + diff stability across operators).
46
+ */
47
+ export function computeConfigDiff(previous, current) {
48
+ const changes = [];
49
+ walkAndCompare('', previous, current, changes);
50
+ changes.sort((a, b) => a.path.localeCompare(b.path));
51
+ return { changed: changes.length > 0, changes };
52
+ }
53
+ function walkAndCompare(prefix, prev, curr, out) {
54
+ // Array fields — compared as ordered list (insertion order matters).
55
+ if (Array.isArray(prev) || Array.isArray(curr)) {
56
+ if (!arraysEqual(prev, curr)) {
57
+ out.push({ path: prefix || '(root)', previous: prev, current: curr });
58
+ }
59
+ return;
60
+ }
61
+ // Object fields — recurse field-by-field. Take the union of keys so
62
+ // additions + removals both surface as drift.
63
+ if (isPlainObject(prev) && isPlainObject(curr)) {
64
+ const keys = new Set([...Object.keys(prev), ...Object.keys(curr)]);
65
+ for (const k of keys) {
66
+ const nextPrefix = prefix ? `${prefix}.${k}` : k;
67
+ walkAndCompare(nextPrefix, prev[k], curr[k], out);
68
+ }
69
+ return;
70
+ }
71
+ // Scalar leaf — compare via strict equality (numbers + strings +
72
+ // booleans are the only scalar types in the config). NaN is treated as
73
+ // unequal to itself, matching JavaScript's `!==` semantics; the loader
74
+ // rejects NaN at validation time so this branch should never fire in
75
+ // production but defending against it costs nothing.
76
+ if (prev !== curr) {
77
+ out.push({ path: prefix || '(root)', previous: prev, current: curr });
78
+ }
79
+ }
80
+ function arraysEqual(a, b) {
81
+ if (!Array.isArray(a) || !Array.isArray(b))
82
+ return false;
83
+ if (a.length !== b.length)
84
+ return false;
85
+ for (let i = 0; i < a.length; i++) {
86
+ if (a[i] !== b[i])
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+ function isPlainObject(value) {
92
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
93
+ }
94
+ /**
95
+ * Append a `SignalIngestionConfigChanged` event to the orchestrator's
96
+ * date-rotated events file. Best-effort: returns `false` on write failure
97
+ * (logged via `warn`) instead of throwing.
98
+ */
99
+ export function writeSignalIngestionConfigChangedEvent(event, opts = {}) {
100
+ const artifactsDir = resolveArtifactsDir(opts);
101
+ const now = opts.now ?? (() => new Date());
102
+ const date = now();
103
+ const stamped = {
104
+ ...event,
105
+ ts: event.ts || date.toISOString(),
106
+ };
107
+ const path = eventsFilePath(artifactsDir, date);
108
+ const line = JSON.stringify(stamped) + '\n';
109
+ try {
110
+ if (!existsSync(dirname(path))) {
111
+ mkdirSync(dirname(path), { recursive: true });
112
+ }
113
+ appendFileSync(path, line, { encoding: 'utf8' });
114
+ return true;
115
+ }
116
+ catch (err) {
117
+ const reason = err instanceof Error ? err.message : String(err);
118
+ opts.warn?.(`[signal-ingestion-governance] events write failed (path=${path}): ${reason}`);
119
+ return false;
120
+ }
121
+ }
122
+ function resolveArtifactsDir(opts) {
123
+ return opts.artifactsDir ?? process.env.ARTIFACTS_DIR ?? join(process.cwd(), 'artifacts');
124
+ }
125
+ /**
126
+ * Resolve the on-disk path for the date-rotated events file. Mirrors
127
+ * `pipeline-cli/src/orchestrator/events.ts#eventsFilePath()` so the two
128
+ * writers append to the same file. Exported so tests + cli-status can
129
+ * derive the same path without duplicating the rotation logic.
130
+ */
131
+ export function eventsFilePath(artifactsDir, date = new Date()) {
132
+ return join(artifactsDir, '_orchestrator', `events-${formatDate(date)}.jsonl`);
133
+ }
134
+ function formatDate(d) {
135
+ // YYYY-MM-DD in UTC — matches `pipeline-cli/src/orchestrator/events.ts`
136
+ // so rotation seams align across writers.
137
+ const yyyy = d.getUTCFullYear().toString().padStart(4, '0');
138
+ const mm = (d.getUTCMonth() + 1).toString().padStart(2, '0');
139
+ const dd = d.getUTCDate().toString().padStart(2, '0');
140
+ return `${yyyy}-${mm}-${dd}`;
141
+ }
142
+ /**
143
+ * Load the signal-ingestion config and emit a `SignalIngestionConfigChanged`
144
+ * event when the loaded config differs from the comparison baseline
145
+ * (defaults, or `previousConfigSnapshot` when supplied).
146
+ *
147
+ * Returns `{config, diff, eventWritten, configPath}`. `eventWritten` is
148
+ * `false` when (a) there was no diff to report, (b) the caller passed
149
+ * `skipEventEmission: true`, or (c) the best-effort write threw.
150
+ *
151
+ * This is the canonical entry point for orchestrator surfaces that want
152
+ * the audit trail. Pure callers that just need the resolved config can
153
+ * keep using `loadSignalIngestionConfig()` directly — they bypass the
154
+ * governance layer entirely.
155
+ */
156
+ export function loadSignalIngestionConfigWithGovernance(options = {}) {
157
+ // Route through loadSignalIngestionConfigWithDeprecations so the
158
+ // canonical governance-aware loader does NOT silently drop legacy
159
+ // `sourceBaselineDriftMultiplier` keys (codex MAJOR on #752 — the
160
+ // basic loader runs resolveFloodingDetection which ignores unknown
161
+ // keys, breaking the one-release-window soft-translation contract).
162
+ const { config, deprecations } = loadSignalIngestionConfigWithDeprecations({
163
+ projectRoot: options.projectRoot,
164
+ configPath: options.configPath,
165
+ });
166
+ const previous = options.previousConfigSnapshot ?? DEFAULT_SIGNAL_INGESTION_CONFIG;
167
+ const comparedAgainst = options.previousConfigSnapshot ? 'previous-load' : 'defaults';
168
+ const diff = computeConfigDiff(previous, config);
169
+ const configPath = options.configPath ??
170
+ (options.projectRoot
171
+ ? join(options.projectRoot, '.ai-sdlc', 'signal-ingestion.yaml')
172
+ : join(process.cwd(), '.ai-sdlc', 'signal-ingestion.yaml'));
173
+ let eventWritten = false;
174
+ if (diff.changed && !options.skipEventEmission) {
175
+ eventWritten = writeSignalIngestionConfigChangedEvent({
176
+ ts: '',
177
+ type: 'SignalIngestionConfigChanged',
178
+ configPath,
179
+ changes: diff.changes,
180
+ comparedAgainst,
181
+ }, {
182
+ artifactsDir: options.artifactsDir,
183
+ now: options.now,
184
+ warn: options.warn,
185
+ });
186
+ }
187
+ return { config, diff, eventWritten, configPath, deprecations };
188
+ }
189
+ //# sourceMappingURL=governance-events.js.map
@@ -0,0 +1,35 @@
1
+ export { type AdapterCredentialInvalidDecision, type AdapterCredentialNotConfiguredDecision, type AdapterCredentialRejectedDecision, type AdapterRequiresCredentialMgmtRfcDecision, type CustomerTier, type ManualSignalIncompleteDecision, type ManualSignalRateLimitExceededDecision, type ManualSignalShareElevatedDecision, type RawSignal, type SignalFetchResult, type SignalResidencyViolationDecision, type SignalSourceAdapter, type SignalSourceName, type SignalTier, } from './types.js';
2
+ export { AdapterCredentialInvalid, AdapterCredentialNotConfigured, AdapterCredentialRejected, AdapterRequiresCredentialMgmtRfc, ManualSignalIncomplete, ManualSignalRateLimitExceeded, SignalSourceUnavailable, UnknownSignalSource, } from './errors.js';
3
+ export { SignalSourceRegistry, fetchSignalsFromAvailableAdapters, getSignalSourceAdapter, } from './registry.js';
4
+ export { SupportTicketSignalSourceAdapter, DEFAULT_SUPPORT_TICKET_ENV_VAR, type SupportTicketAdapterOptions, } from './adapters/support-ticket.js';
5
+ export { CommunityThreadSignalSourceAdapter, DEFAULT_COMMUNITY_THREAD_ENV_VAR, type CommunityThreadAdapterOptions, } from './adapters/community-thread.js';
6
+ export { InAppFeedbackSignalSourceAdapter, DEFAULT_IN_APP_FEEDBACK_ENV_VAR, type InAppFeedbackAdapterOptions, } from './adapters/in-app-feedback.js';
7
+ export { ManualSignalSourceAdapter, DEFAULT_MANUAL_DAILY_CAP_PER_OPERATOR, utcDateKey, type ManualSignalInput, type ManualSignalSourceAdapterOptions, } from './adapters/manual.js';
8
+ export { computeManualShareMetric, defaultIsManualSignal, DEFAULT_MANUAL_SHARE_MIN_POPULATION, DEFAULT_MANUAL_SHARE_WARNING_THRESHOLD, DEFAULT_MANUAL_SHARE_WINDOW_DAYS, type ManualShareMetricOptions, type ManualShareMetricResult, } from './manual-share-metric.js';
9
+ export { classifySignals, computeRecencyDecay, computeSignalWeight, resolveCustomerTier, resolveIcpResonance, tokenize, type ClassificationResult, type ClassifiedSignal, type ClassifySignalsOptions, type CustomerTierRegistry, type ICPResonance, type SignalLanguageUnsupportedDecision, } from './classifier.js';
10
+ export { loadSignalIngestionConfig, loadSignalIngestionConfigWithDeprecations, DEFAULT_SIGNAL_INGESTION_CONFIG, DEFAULT_SIGNAL_INGESTION_CONFIG_PATH, SignalIngestionConfigError, type ClusteringConfig, type D1CompositionWeights, type FloodingConfig, type FloodingDetectionConfig, type FloodingQuarantineConfig, type IcpResonanceWeights, type LanguageDetectionConfig, type LoadSignalIngestionConfigOptions, type LoadSignalIngestionConfigWithDeprecationsResult, type ManualEntryConfig, type ManualEntryQualityMetricConfig, type ResidencyEnforcementConfig, type SaResonanceThresholds, type SignalIngestionConfig, type SignalIngestionConfigDeprecatedFieldDecision, type Tier2SignificanceThreshold, type TierMultipliers, } from './config.js';
11
+ export { computeConfigDiff, eventsFilePath as signalIngestionEventsFilePath, loadSignalIngestionConfigWithGovernance, writeSignalIngestionConfigChangedEvent, type LoadConfigWithGovernanceOptions, type LoadConfigWithGovernanceResult, type SignalIngestionConfigChange, type SignalIngestionConfigChangedEvent, type SignalIngestionConfigDiff, type WriteConfigChangeEventOpts, } from './governance-events.js';
12
+ export { clusterSignals, clusterSignalsWithResidency, computeClusterId, cosineSimilarity, type ClusterSignalsOptions, type ClusterSignalsWithResidencyOptions, type ClusteredSignalInput, type ClusteringAlgorithmUsed, type ClusteringResult, type ClusteringResultWithResidency, type DemandCluster, } from './clustering.js';
13
+ export { clusterRequiresSegregation, composePostures, groupCostByRegion, makeStoredSignalRecord, partitionSignalsByRegion, readSignalRecordWithAudit, type CostAttributionRow, type CostByRegionBreakdown, type CrossRegionReadAuditEntry, type PostureRegimeInput, type StoredSignalRecord, } from './residency.js';
14
+ export { assessClusterSignificance, assessTier2Significance, checkSignalResidency, classifySaResonance, computeBaselineStat, computeZScore, DEFAULT_FLOODING_DETECTION_CONFIG, detectFlooding, filterSignalsByResidency, InMemoryQuarantineStore, isSignalQuarantined, SA_WEIGHT_MULTIPLIERS, unquarantineFlooded, type AssessClusterSignificanceOptions, type AssessClusterSignificanceResult, type BaselineStat, type DetectFloodingOptions, type FloodingDetectionResult, type FloodingDetectionStatus, type FloodingSourceFlag, type PerSourceBaseline, type QuarantineEntry, type QuarantineStore, type ResidencyRegimeDeclaration, type SaResonanceBucket, type SignalFloodingDetectedDecision, type SignalFloodingFalsePositiveDecision, type SignalLowSaForReviewDecision, type SignalOutOfScopeDecision, type SignalResidencyCheck, type SignificanceAssessedCluster, type Tier2SignificanceReasons, type Tier2SignificanceState, type UnquarantineFloodedOptions, } from './significance.js';
15
+ export { aggregateD1FromClusters, composeD1Inputs, computeClusterD1, enrichDemandSignalFromClusters, type AggregatedD1Result, type ClusterD1Score, type ClusterMatcher, type ComposeD1InputsArgs, type ComposedD1Result, type ComputeClusterD1Options, type EnrichDemandSignalArgs, type EnrichDemandSignalResult, } from './d1.js';
16
+ import { SignalSourceRegistry } from './registry.js';
17
+ /**
18
+ * Construct the default signal-source registry with the RFC-0030 OQ-13.1
19
+ * v0.3 v1 adapter set (env-var-based only):
20
+ * - `signal-source-support-ticket` (Zendesk PAT via `SIGNAL_ZENDESK_PAT`)
21
+ * - `signal-source-community-thread` (Discord / Slack bot token via
22
+ * `SIGNAL_COMMUNITY_BOT_TOKEN` / custom)
23
+ * - `signal-source-in-app-feedback` (API key via `SIGNAL_IN_APP_FEEDBACK_API_KEY`)
24
+ * - `signal-source-manual` (no auth)
25
+ *
26
+ * OAuth-required adapters (full Salesforce / HubSpot / OAuth-scoped Zendesk)
27
+ * are NOT included; they defer to the future credential-management RFC and
28
+ * would be REFUSED at registration by the `requiresOAuth = true` gate.
29
+ *
30
+ * The default constructions DO NOT enable env-var probing (`probeEnvVar:
31
+ * false`) so the in-memory test pattern continues to work. Production
32
+ * deployments should pass `probeEnvVar: true` explicitly per adapter.
33
+ */
34
+ export declare function createDefaultSignalSourceRegistry(): SignalSourceRegistry;
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,53 @@
1
+ export { AdapterCredentialInvalid, AdapterCredentialNotConfigured, AdapterCredentialRejected, AdapterRequiresCredentialMgmtRfc, ManualSignalIncomplete, ManualSignalRateLimitExceeded, SignalSourceUnavailable, UnknownSignalSource, } from './errors.js';
2
+ export { SignalSourceRegistry, fetchSignalsFromAvailableAdapters, getSignalSourceAdapter, } from './registry.js';
3
+ export { SupportTicketSignalSourceAdapter, DEFAULT_SUPPORT_TICKET_ENV_VAR, } from './adapters/support-ticket.js';
4
+ export { CommunityThreadSignalSourceAdapter, DEFAULT_COMMUNITY_THREAD_ENV_VAR, } from './adapters/community-thread.js';
5
+ export { InAppFeedbackSignalSourceAdapter, DEFAULT_IN_APP_FEEDBACK_ENV_VAR, } from './adapters/in-app-feedback.js';
6
+ export { ManualSignalSourceAdapter, DEFAULT_MANUAL_DAILY_CAP_PER_OPERATOR, utcDateKey, } from './adapters/manual.js';
7
+ // RFC-0030 OQ-13.4 v0.3 — manual-share quality metric
8
+ export { computeManualShareMetric, defaultIsManualSignal, DEFAULT_MANUAL_SHARE_MIN_POPULATION, DEFAULT_MANUAL_SHARE_WARNING_THRESHOLD, DEFAULT_MANUAL_SHARE_WINDOW_DAYS, } from './manual-share-metric.js';
9
+ // RFC-0030 Phase 2 — classification
10
+ export { classifySignals, computeRecencyDecay, computeSignalWeight, resolveCustomerTier, resolveIcpResonance, tokenize, } from './classifier.js';
11
+ export { loadSignalIngestionConfig, loadSignalIngestionConfigWithDeprecations, DEFAULT_SIGNAL_INGESTION_CONFIG, DEFAULT_SIGNAL_INGESTION_CONFIG_PATH, SignalIngestionConfigError, } from './config.js';
12
+ // RFC-0030 §11 / AISDLC-348 Phase 6 — governance event logging
13
+ export { computeConfigDiff, eventsFilePath as signalIngestionEventsFilePath, loadSignalIngestionConfigWithGovernance, writeSignalIngestionConfigChangedEvent, } from './governance-events.js';
14
+ // RFC-0030 Phase 3 — clustering
15
+ export { clusterSignals, clusterSignalsWithResidency, computeClusterId, cosineSimilarity, } from './clustering.js';
16
+ // RFC-0030 OQ-13.3 re-walkthrough — per-stage residency enforcement +
17
+ // multi-posture composition (AISDLC-432).
18
+ export { clusterRequiresSegregation, composePostures, groupCostByRegion, makeStoredSignalRecord, partitionSignalsByRegion, readSignalRecordWithAudit, } from './residency.js';
19
+ // RFC-0030 Phase 4 — significance threshold + SA resonance filter + flooding + residency
20
+ export { assessClusterSignificance, assessTier2Significance, checkSignalResidency, classifySaResonance, computeBaselineStat, computeZScore, DEFAULT_FLOODING_DETECTION_CONFIG, detectFlooding, filterSignalsByResidency, InMemoryQuarantineStore, isSignalQuarantined, SA_WEIGHT_MULTIPLIERS, unquarantineFlooded, } from './significance.js';
21
+ // RFC-0030 Phase 5 — D1 formula reformulation + RFC-0008 PPA integration
22
+ export { aggregateD1FromClusters, composeD1Inputs, computeClusterD1, enrichDemandSignalFromClusters, } from './d1.js';
23
+ import { CommunityThreadSignalSourceAdapter } from './adapters/community-thread.js';
24
+ import { InAppFeedbackSignalSourceAdapter } from './adapters/in-app-feedback.js';
25
+ import { ManualSignalSourceAdapter } from './adapters/manual.js';
26
+ import { SupportTicketSignalSourceAdapter } from './adapters/support-ticket.js';
27
+ import { SignalSourceRegistry } from './registry.js';
28
+ /**
29
+ * Construct the default signal-source registry with the RFC-0030 OQ-13.1
30
+ * v0.3 v1 adapter set (env-var-based only):
31
+ * - `signal-source-support-ticket` (Zendesk PAT via `SIGNAL_ZENDESK_PAT`)
32
+ * - `signal-source-community-thread` (Discord / Slack bot token via
33
+ * `SIGNAL_COMMUNITY_BOT_TOKEN` / custom)
34
+ * - `signal-source-in-app-feedback` (API key via `SIGNAL_IN_APP_FEEDBACK_API_KEY`)
35
+ * - `signal-source-manual` (no auth)
36
+ *
37
+ * OAuth-required adapters (full Salesforce / HubSpot / OAuth-scoped Zendesk)
38
+ * are NOT included; they defer to the future credential-management RFC and
39
+ * would be REFUSED at registration by the `requiresOAuth = true` gate.
40
+ *
41
+ * The default constructions DO NOT enable env-var probing (`probeEnvVar:
42
+ * false`) so the in-memory test pattern continues to work. Production
43
+ * deployments should pass `probeEnvVar: true` explicitly per adapter.
44
+ */
45
+ export function createDefaultSignalSourceRegistry() {
46
+ const registry = new SignalSourceRegistry();
47
+ registry.register(new SupportTicketSignalSourceAdapter());
48
+ registry.register(new CommunityThreadSignalSourceAdapter());
49
+ registry.register(new InAppFeedbackSignalSourceAdapter());
50
+ registry.register(new ManualSignalSourceAdapter());
51
+ return registry;
52
+ }
53
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * RFC-0030 OQ-13.4 v0.3 re-walkthrough — manual-share quality metric.
3
+ *
4
+ * Tracks the rolling `manualSignals / totalSignals` ratio over a configurable
5
+ * window (default 7d). When the share exceeds the configured threshold
6
+ * (default 0.30) on a population that's large enough to be meaningful, emit
7
+ * `Decision: manual-signal-share-elevated` — a WARNING (not a block) that
8
+ * the pipeline is acting as a data-entry tool rather than automated
9
+ * demand-detection (architectural anti-pattern).
10
+ *
11
+ * Design decisions:
12
+ * - Window is computed against `asOf` (defaults to `new Date()`) so tests
13
+ * can drive a deterministic clock.
14
+ * - Signals are bucketed by `sourceTimestamp` (NOT receipt time); this
15
+ * matches the spirit of the metric ("what fraction of recent SOURCE
16
+ * activity was manual"). Operator-attested manual signals carry their
17
+ * sourceTimestamp = observation time, so the bucketing is honest.
18
+ * - When `totalSignals == 0` over the window, returns `manualShare: 0` and
19
+ * `elevated: false` (no division-by-zero, no false alarms on empty windows).
20
+ * - The metric does NOT mutate signals — it observes a population and
21
+ * returns a verdict. Caller (typically the pipeline orchestrator step
22
+ * that wires Phase 4 outputs) emits the Decision when `elevated` is true.
23
+ */
24
+ import type { ManualSignalShareElevatedDecision, RawSignal } from './types.js';
25
+ /** Default window (rolling) over which the manual-share ratio is computed. */
26
+ export declare const DEFAULT_MANUAL_SHARE_WINDOW_DAYS = 7;
27
+ /** Default warning threshold (manual / total) above which the Decision fires. */
28
+ export declare const DEFAULT_MANUAL_SHARE_WARNING_THRESHOLD = 0.3;
29
+ /**
30
+ * Minimum total-signals count before we'll fire the elevated Decision.
31
+ * Prevents thrashing on tiny populations (1 manual + 1 total = 100% share
32
+ * fires spuriously). Tunable per-deployment if the framework default doesn't
33
+ * fit.
34
+ */
35
+ export declare const DEFAULT_MANUAL_SHARE_MIN_POPULATION = 5;
36
+ /**
37
+ * Per-signal source classifier: returns `true` when the signal came from a
38
+ * manual-entry adapter. The check is name-based to keep the helper
39
+ * decoupled from the `ManualSignalSourceAdapter` class — sources are
40
+ * identified by their `SignalSourceName`, and `signal-source-manual` is the
41
+ * canonical name.
42
+ *
43
+ * Tests can pass a custom predicate via `ManualShareMetricOptions.isManual`
44
+ * when modelling derivative manual-entry adapters.
45
+ */
46
+ export declare function defaultIsManualSignal(signal: RawSignal): boolean;
47
+ /** Inputs for `computeManualShareMetric()`. */
48
+ export interface ManualShareMetricOptions {
49
+ /** Rolling window in days. Defaults to `DEFAULT_MANUAL_SHARE_WINDOW_DAYS` (7). */
50
+ windowDays?: number;
51
+ /** Warning threshold. Defaults to `DEFAULT_MANUAL_SHARE_WARNING_THRESHOLD` (0.30). */
52
+ shareWarningThreshold?: number;
53
+ /** Reference time for the rolling window. Defaults to `new Date()`. */
54
+ asOf?: Date;
55
+ /**
56
+ * Minimum total-signal count required before the elevated Decision can
57
+ * fire. Defaults to `DEFAULT_MANUAL_SHARE_MIN_POPULATION` (5). Prevents
58
+ * spurious alarms on tiny populations.
59
+ */
60
+ minPopulation?: number;
61
+ /** Override the manual-signal classifier. Defaults to `defaultIsManualSignal`. */
62
+ isManual?: (signal: RawSignal) => boolean;
63
+ }
64
+ /** Result of `computeManualShareMetric()`. */
65
+ export interface ManualShareMetricResult {
66
+ /** Rolling manual / total ratio over the window. `0` when totalSignals is 0. */
67
+ manualShare: number;
68
+ /** Number of manual signals in the window. */
69
+ manualSignals: number;
70
+ /** Total number of signals in the window. */
71
+ totalSignals: number;
72
+ /** The window the metric was computed over (in days). */
73
+ windowDays: number;
74
+ /** Whether the elevated Decision should fire. */
75
+ elevated: boolean;
76
+ /**
77
+ * The Decision record, populated IFF `elevated` is `true`. Caller can
78
+ * forward this directly into the catalog without re-constructing the
79
+ * envelope.
80
+ */
81
+ decision?: ManualSignalShareElevatedDecision;
82
+ }
83
+ /**
84
+ * Compute the rolling manual-share metric on the supplied signal population.
85
+ *
86
+ * Bucketing rule: a signal is in the window IFF
87
+ * `signal.sourceTimestamp >= asOf - windowDays`.
88
+ *
89
+ * The result is suitable for streaming back to the operator as a quality
90
+ * signal AND for forwarding to the RFC-0035 catalog when `elevated` fires.
91
+ */
92
+ export declare function computeManualShareMetric(signals: readonly RawSignal[], options?: ManualShareMetricOptions): ManualShareMetricResult;
93
+ //# sourceMappingURL=manual-share-metric.d.ts.map
@@ -0,0 +1,106 @@
1
+ /**
2
+ * RFC-0030 OQ-13.4 v0.3 re-walkthrough — manual-share quality metric.
3
+ *
4
+ * Tracks the rolling `manualSignals / totalSignals` ratio over a configurable
5
+ * window (default 7d). When the share exceeds the configured threshold
6
+ * (default 0.30) on a population that's large enough to be meaningful, emit
7
+ * `Decision: manual-signal-share-elevated` — a WARNING (not a block) that
8
+ * the pipeline is acting as a data-entry tool rather than automated
9
+ * demand-detection (architectural anti-pattern).
10
+ *
11
+ * Design decisions:
12
+ * - Window is computed against `asOf` (defaults to `new Date()`) so tests
13
+ * can drive a deterministic clock.
14
+ * - Signals are bucketed by `sourceTimestamp` (NOT receipt time); this
15
+ * matches the spirit of the metric ("what fraction of recent SOURCE
16
+ * activity was manual"). Operator-attested manual signals carry their
17
+ * sourceTimestamp = observation time, so the bucketing is honest.
18
+ * - When `totalSignals == 0` over the window, returns `manualShare: 0` and
19
+ * `elevated: false` (no division-by-zero, no false alarms on empty windows).
20
+ * - The metric does NOT mutate signals — it observes a population and
21
+ * returns a verdict. Caller (typically the pipeline orchestrator step
22
+ * that wires Phase 4 outputs) emits the Decision when `elevated` is true.
23
+ */
24
+ /** Default window (rolling) over which the manual-share ratio is computed. */
25
+ export const DEFAULT_MANUAL_SHARE_WINDOW_DAYS = 7;
26
+ /** Default warning threshold (manual / total) above which the Decision fires. */
27
+ export const DEFAULT_MANUAL_SHARE_WARNING_THRESHOLD = 0.3;
28
+ /**
29
+ * Minimum total-signals count before we'll fire the elevated Decision.
30
+ * Prevents thrashing on tiny populations (1 manual + 1 total = 100% share
31
+ * fires spuriously). Tunable per-deployment if the framework default doesn't
32
+ * fit.
33
+ */
34
+ export const DEFAULT_MANUAL_SHARE_MIN_POPULATION = 5;
35
+ /**
36
+ * Per-signal source classifier: returns `true` when the signal came from a
37
+ * manual-entry adapter. The check is name-based to keep the helper
38
+ * decoupled from the `ManualSignalSourceAdapter` class — sources are
39
+ * identified by their `SignalSourceName`, and `signal-source-manual` is the
40
+ * canonical name.
41
+ *
42
+ * Tests can pass a custom predicate via `ManualShareMetricOptions.isManual`
43
+ * when modelling derivative manual-entry adapters.
44
+ */
45
+ export function defaultIsManualSignal(signal) {
46
+ const adapterName = signal.metadata?.['adapterName'];
47
+ if (adapterName === 'signal-source-manual')
48
+ return true;
49
+ // Heuristic fallback: presence of `attestedBy` is the structural signature
50
+ // of a manual-entered signal even when metadata is missing the adapterName.
51
+ return typeof signal.attestedBy === 'string' && signal.attestedBy.trim().length > 0;
52
+ }
53
+ /**
54
+ * Compute the rolling manual-share metric on the supplied signal population.
55
+ *
56
+ * Bucketing rule: a signal is in the window IFF
57
+ * `signal.sourceTimestamp >= asOf - windowDays`.
58
+ *
59
+ * The result is suitable for streaming back to the operator as a quality
60
+ * signal AND for forwarding to the RFC-0035 catalog when `elevated` fires.
61
+ */
62
+ export function computeManualShareMetric(signals, options = {}) {
63
+ const windowDays = options.windowDays ?? DEFAULT_MANUAL_SHARE_WINDOW_DAYS;
64
+ const threshold = options.shareWarningThreshold ?? DEFAULT_MANUAL_SHARE_WARNING_THRESHOLD;
65
+ const asOf = options.asOf ?? new Date();
66
+ const minPopulation = options.minPopulation ?? DEFAULT_MANUAL_SHARE_MIN_POPULATION;
67
+ const isManual = options.isManual ?? defaultIsManualSignal;
68
+ const windowStart = new Date(asOf.getTime() - windowDays * 24 * 60 * 60 * 1000);
69
+ let manualCount = 0;
70
+ let totalCount = 0;
71
+ for (const s of signals) {
72
+ if (s.sourceTimestamp < windowStart)
73
+ continue;
74
+ if (s.sourceTimestamp > asOf)
75
+ continue;
76
+ totalCount += 1;
77
+ if (isManual(s))
78
+ manualCount += 1;
79
+ }
80
+ const manualShare = totalCount === 0 ? 0 : manualCount / totalCount;
81
+ const elevated = totalCount >= minPopulation && manualShare > threshold;
82
+ const result = {
83
+ manualShare,
84
+ manualSignals: manualCount,
85
+ totalSignals: totalCount,
86
+ windowDays,
87
+ elevated,
88
+ };
89
+ if (elevated) {
90
+ result.decision = {
91
+ type: 'Decision',
92
+ decision: 'manual-signal-share-elevated',
93
+ manualShare,
94
+ threshold,
95
+ windowDays,
96
+ manualSignals: manualCount,
97
+ totalSignals: totalCount,
98
+ message: `Manual signal share elevated: ${(manualShare * 100).toFixed(1)}% over ` +
99
+ `last ${windowDays}d (${manualCount}/${totalCount}); threshold is ${(threshold * 100).toFixed(0)}%. ` +
100
+ `This may indicate the pipeline is being used as a data-entry tool rather than automated ` +
101
+ `demand-detection. Review adapter coverage + automation gaps.`,
102
+ };
103
+ }
104
+ return result;
105
+ }
106
+ //# sourceMappingURL=manual-share-metric.js.map