@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.
- package/dist/__test-helpers/git-env.d.ts +40 -0
- package/dist/__test-helpers/git-env.js +73 -0
- package/dist/adapters.d.ts +42 -3
- package/dist/adapters.js +133 -3
- package/dist/admission-composite.d.ts +134 -1
- package/dist/admission-composite.js +170 -6
- package/dist/admission-enrichment.d.ts +12 -3
- package/dist/admission-enrichment.js +71 -10
- package/dist/admission-hc.d.ts +20 -1
- package/dist/admission-hc.js +12 -1
- package/dist/admission-score.d.ts +44 -0
- package/dist/admission-score.js +11 -2
- package/dist/backlog-adapter.d.ts +45 -0
- package/dist/backlog-adapter.js +145 -1
- package/dist/calibration.d.ts +81 -0
- package/dist/calibration.js +76 -0
- package/dist/cli/commands/git-remote.d.ts +25 -0
- package/dist/cli/commands/git-remote.js +80 -1
- package/dist/cli/commands/init-features.d.ts +507 -0
- package/dist/cli/commands/init-features.js +1100 -0
- package/dist/cli/commands/init-templates.d.ts +301 -0
- package/dist/cli/commands/init-templates.js +1341 -0
- package/dist/cli/commands/init.d.ts +67 -0
- package/dist/cli/commands/init.js +301 -2
- package/dist/cli/commands/run.js +9 -1
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.js +42 -8
- package/dist/compliance/composer.d.ts +79 -0
- package/dist/compliance/composer.js +258 -0
- package/dist/compliance/errors.d.ts +64 -0
- package/dist/compliance/errors.js +85 -0
- package/dist/compliance/loader.d.ts +52 -0
- package/dist/compliance/loader.js +124 -0
- package/dist/compliance/types.d.ts +184 -0
- package/dist/compliance/types.js +41 -0
- package/dist/compliance-clearance.d.ts +269 -0
- package/dist/compliance-clearance.js +269 -0
- package/dist/config.js +17 -0
- package/dist/cost-tracker.d.ts +22 -0
- package/dist/cost-tracker.js +41 -0
- package/dist/database/adapters/external.js +5 -1
- package/dist/design-authority.d.ts +13 -0
- package/dist/design-authority.js +13 -0
- package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
- package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
- package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
- package/dist/embedding/consumers/tessellation-drift.js +76 -0
- package/dist/embedding/cross-provider.d.ts +78 -0
- package/dist/embedding/cross-provider.js +75 -0
- package/dist/embedding/deprecation.d.ts +151 -0
- package/dist/embedding/deprecation.js +229 -0
- package/dist/embedding/errors.d.ts +90 -0
- package/dist/embedding/errors.js +150 -0
- package/dist/embedding/index.d.ts +29 -0
- package/dist/embedding/index.js +24 -0
- package/dist/embedding/pipeline-load.d.ts +146 -0
- package/dist/embedding/pipeline-load.js +178 -0
- package/dist/embedding/registry.d.ts +45 -0
- package/dist/embedding/registry.js +61 -0
- package/dist/embedding/stale-vector.d.ts +110 -0
- package/dist/embedding/stale-vector.js +92 -0
- package/dist/embedding/storage/index.d.ts +51 -0
- package/dist/embedding/storage/index.js +43 -0
- package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
- package/dist/embedding/storage/jsonl-backend.js +332 -0
- package/dist/embedding/storage/types.d.ts +135 -0
- package/dist/embedding/storage/types.js +13 -0
- package/dist/embedding/types.d.ts +180 -0
- package/dist/embedding/types.js +10 -0
- package/dist/execute.d.ts +29 -2
- package/dist/execute.js +179 -35
- package/dist/index.d.ts +14 -2
- package/dist/index.js +19 -2
- package/dist/journey/inheritance-validator.d.ts +396 -0
- package/dist/journey/inheritance-validator.js +370 -0
- package/dist/journey/state-id-drift-rule.d.ts +137 -0
- package/dist/journey/state-id-drift-rule.js +245 -0
- package/dist/journey-sa2-router.d.ts +395 -0
- package/dist/journey-sa2-router.js +308 -0
- package/dist/models/classifier.d.ts +3 -1
- package/dist/models/classifier.js +61 -5
- package/dist/pillar-breakdown.d.ts +23 -1
- package/dist/pillar-breakdown.js +9 -0
- package/dist/runners/review-agent.js +6 -2
- package/dist/runners/runner-registry.d.ts +36 -0
- package/dist/runners/runner-registry.js +90 -0
- package/dist/runtime/attestations.d.ts +663 -11
- package/dist/runtime/attestations.js +971 -24
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/parallelism-flag.d.ts +21 -3
- package/dist/runtime/parallelism-flag.js +27 -6
- package/dist/runtime/port-allocator.d.ts +11 -0
- package/dist/runtime/port-allocator.js +30 -0
- package/dist/sa-scoring/layer3-llm.js +6 -1
- package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
- package/dist/sa-scoring/revision-proposal-config.js +198 -0
- package/dist/sa-scoring/revision-proposal.d.ts +285 -0
- package/dist/sa-scoring/revision-proposal.js +417 -0
- package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
- package/dist/signal-ingestion/adapters/community-thread.js +55 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
- package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
- package/dist/signal-ingestion/adapters/manual.js +112 -0
- package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
- package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
- package/dist/signal-ingestion/classifier.d.ts +205 -0
- package/dist/signal-ingestion/classifier.js +494 -0
- package/dist/signal-ingestion/clustering-types.d.ts +36 -0
- package/dist/signal-ingestion/clustering-types.js +14 -0
- package/dist/signal-ingestion/clustering.d.ts +200 -0
- package/dist/signal-ingestion/clustering.js +413 -0
- package/dist/signal-ingestion/config.d.ts +351 -0
- package/dist/signal-ingestion/config.js +587 -0
- package/dist/signal-ingestion/d1.d.ts +252 -0
- package/dist/signal-ingestion/d1.js +235 -0
- package/dist/signal-ingestion/errors.d.ts +73 -0
- package/dist/signal-ingestion/errors.js +108 -0
- package/dist/signal-ingestion/governance-events.d.ts +181 -0
- package/dist/signal-ingestion/governance-events.js +189 -0
- package/dist/signal-ingestion/index.d.ts +35 -0
- package/dist/signal-ingestion/index.js +53 -0
- package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
- package/dist/signal-ingestion/manual-share-metric.js +106 -0
- package/dist/signal-ingestion/registry.d.ts +40 -0
- package/dist/signal-ingestion/registry.js +137 -0
- package/dist/signal-ingestion/residency.d.ts +227 -0
- package/dist/signal-ingestion/residency.js +238 -0
- package/dist/signal-ingestion/significance.d.ts +554 -0
- package/dist/signal-ingestion/significance.js +555 -0
- package/dist/signal-ingestion/types.d.ts +191 -0
- package/dist/signal-ingestion/types.js +8 -0
- package/dist/substrate/drift-composition.d.ts +270 -0
- package/dist/substrate/drift-composition.js +306 -0
- package/dist/substrate/drift-tui-surface.d.ts +61 -0
- package/dist/substrate/drift-tui-surface.js +102 -0
- package/dist/substrate/identity-class.d.ts +176 -0
- package/dist/substrate/identity-class.js +201 -0
- package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
- package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
- package/dist/tessellation/rule-registry.d.ts +269 -0
- package/dist/tessellation/rule-registry.js +92 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
- package/dist/tessellation-admission.d.ts +162 -0
- package/dist/tessellation-admission.js +146 -0
- package/dist/tessellation-drift.d.ts +246 -0
- package/dist/tessellation-drift.js +250 -0
- package/dist/validate-config.js +13 -0
- package/dist/validate-issue.js +2 -2
- package/dist/variant/cardinality-activation.d.ts +126 -0
- package/dist/variant/cardinality-activation.js +101 -0
- package/dist/variant/deprecation-lifecycle.d.ts +184 -0
- package/dist/variant/deprecation-lifecycle.js +208 -0
- package/dist/variant/drift-extension.d.ts +136 -0
- package/dist/variant/drift-extension.js +164 -0
- package/dist/variant/engineering-review.d.ts +185 -0
- package/dist/variant/engineering-review.js +142 -0
- package/dist/variant/index.d.ts +32 -0
- package/dist/variant/index.js +32 -0
- package/dist/variant/inheritance-validator.d.ts +165 -0
- package/dist/variant/inheritance-validator.js +139 -0
- package/dist/variant/internal-adopter/index.d.ts +11 -0
- package/dist/variant/internal-adopter/index.js +10 -0
- package/dist/variant/internal-adopter/products.d.ts +156 -0
- package/dist/variant/internal-adopter/products.js +366 -0
- package/dist/variant-admission.d.ts +316 -0
- package/dist/variant-admission.js +247 -0
- 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
|