@ai-sdlc/orchestrator 0.5.0 → 0.9.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 (227) hide show
  1. package/dist/action-enforcement.d.ts +26 -0
  2. package/dist/action-enforcement.js +70 -0
  3. package/dist/admission-composite.d.ts +67 -0
  4. package/dist/admission-composite.js +158 -0
  5. package/dist/admission-enrichment.d.ts +142 -0
  6. package/dist/admission-enrichment.js +331 -0
  7. package/dist/admission-hc.d.ts +62 -0
  8. package/dist/admission-hc.js +83 -0
  9. package/dist/admission-score.d.ts +148 -0
  10. package/dist/admission-score.js +237 -0
  11. package/dist/analysis/analyzer.js +3 -2
  12. package/dist/analysis/convention-detector.d.ts +85 -2
  13. package/dist/analysis/convention-detector.js +375 -70
  14. package/dist/analysis/diff-analyzer.d.ts +33 -0
  15. package/dist/analysis/diff-analyzer.js +122 -0
  16. package/dist/analysis/hotspot-analyzer.js +3 -1
  17. package/dist/analysis/index.d.ts +2 -1
  18. package/dist/analysis/index.js +2 -1
  19. package/dist/artifacts/index.d.ts +65 -0
  20. package/dist/artifacts/index.js +142 -0
  21. package/dist/backlog-adapter.d.ts +121 -0
  22. package/dist/backlog-adapter.js +438 -0
  23. package/dist/calibration.d.ts +43 -0
  24. package/dist/calibration.js +76 -0
  25. package/dist/cli/commands/agents.d.ts +24 -0
  26. package/dist/cli/commands/agents.js +66 -1
  27. package/dist/cli/commands/git-remote.d.ts +49 -0
  28. package/dist/cli/commands/git-remote.js +91 -0
  29. package/dist/cli/commands/health.d.ts +4 -0
  30. package/dist/cli/commands/health.js +13 -1
  31. package/dist/cli/commands/init.d.ts +26 -0
  32. package/dist/cli/commands/init.js +164 -22
  33. package/dist/cli/commands/mcp-setup.d.ts +31 -0
  34. package/dist/cli/commands/mcp-setup.js +78 -8
  35. package/dist/cli/formatters/table.js +15 -2
  36. package/dist/cli/index.d.ts +14 -1
  37. package/dist/cli/index.js +81 -20
  38. package/dist/cli/versions.d.ts +57 -0
  39. package/dist/cli/versions.js +128 -0
  40. package/dist/code-area-classifier.d.ts +21 -0
  41. package/dist/code-area-classifier.js +48 -0
  42. package/dist/config.d.ts +33 -1
  43. package/dist/config.js +78 -8
  44. package/dist/cycle-utils.d.ts +51 -0
  45. package/dist/cycle-utils.js +77 -0
  46. package/dist/database/adapters/external.d.ts +24 -0
  47. package/dist/database/adapters/external.js +80 -0
  48. package/dist/database/adapters/neon.d.ts +41 -0
  49. package/dist/database/adapters/neon.js +98 -0
  50. package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
  51. package/dist/database/adapters/pg-snapshot-restore.js +68 -0
  52. package/dist/database/adapters/sqlite-copy.d.ts +32 -0
  53. package/dist/database/adapters/sqlite-copy.js +145 -0
  54. package/dist/database/connection-injection.d.ts +35 -0
  55. package/dist/database/connection-injection.js +93 -0
  56. package/dist/database/index.d.ts +12 -0
  57. package/dist/database/index.js +23 -0
  58. package/dist/database/registry.d.ts +13 -0
  59. package/dist/database/registry.js +27 -0
  60. package/dist/database/topology.d.ts +52 -0
  61. package/dist/database/topology.js +44 -0
  62. package/dist/database/types.d.ts +89 -0
  63. package/dist/database/types.js +26 -0
  64. package/dist/defaults.d.ts +5 -0
  65. package/dist/defaults.js +5 -0
  66. package/dist/design-authority.d.ts +40 -0
  67. package/dist/design-authority.js +71 -0
  68. package/dist/design-lookahead.d.ts +64 -0
  69. package/dist/design-lookahead.js +86 -0
  70. package/dist/design-quality-trend.d.ts +87 -0
  71. package/dist/design-quality-trend.js +190 -0
  72. package/dist/design-system-context.d.ts +46 -0
  73. package/dist/design-system-context.js +80 -0
  74. package/dist/design-system-correction-loop.d.ts +64 -0
  75. package/dist/design-system-correction-loop.js +128 -0
  76. package/dist/design-system-metrics.d.ts +61 -0
  77. package/dist/design-system-metrics.js +104 -0
  78. package/dist/design-system-stewardship.d.ts +22 -0
  79. package/dist/design-system-stewardship.js +85 -0
  80. package/dist/design-system-validation.d.ts +37 -0
  81. package/dist/design-system-validation.js +88 -0
  82. package/dist/dispatch/index.d.ts +4 -0
  83. package/dist/dispatch/index.js +4 -0
  84. package/dist/dispatch/merge-gate.d.ts +46 -0
  85. package/dist/dispatch/merge-gate.js +90 -0
  86. package/dist/dispatch/requeue.d.ts +57 -0
  87. package/dist/dispatch/requeue.js +131 -0
  88. package/dist/dispatch/worker-pool.d.ts +62 -0
  89. package/dist/dispatch/worker-pool.js +60 -0
  90. package/dist/execute.d.ts +40 -0
  91. package/dist/execute.js +295 -30
  92. package/dist/fix-ci.js +40 -3
  93. package/dist/fix-review.d.ts +66 -0
  94. package/dist/fix-review.js +448 -0
  95. package/dist/harness/adapters/claude-code.d.ts +29 -0
  96. package/dist/harness/adapters/claude-code.js +191 -0
  97. package/dist/harness/adapters/codex.d.ts +25 -0
  98. package/dist/harness/adapters/codex.js +61 -0
  99. package/dist/harness/independence.d.ts +51 -0
  100. package/dist/harness/independence.js +67 -0
  101. package/dist/harness/index.d.ts +14 -0
  102. package/dist/harness/index.js +20 -0
  103. package/dist/harness/registry.d.ts +17 -0
  104. package/dist/harness/registry.js +31 -0
  105. package/dist/harness/types.d.ts +123 -0
  106. package/dist/harness/types.js +8 -0
  107. package/dist/harness/version-probe.d.ts +14 -0
  108. package/dist/harness/version-probe.js +123 -0
  109. package/dist/index.d.ts +39 -5
  110. package/dist/index.js +48 -3
  111. package/dist/models/classifier.d.ts +76 -0
  112. package/dist/models/classifier.js +221 -0
  113. package/dist/models/index.d.ts +3 -0
  114. package/dist/models/index.js +3 -0
  115. package/dist/models/registry.d.ts +97 -0
  116. package/dist/models/registry.js +173 -0
  117. package/dist/pillar-breakdown.d.ts +63 -0
  118. package/dist/pillar-breakdown.js +153 -0
  119. package/dist/pipeline-cycle-detector.d.ts +70 -0
  120. package/dist/pipeline-cycle-detector.js +111 -0
  121. package/dist/priority.d.ts +7 -76
  122. package/dist/priority.js +18 -5
  123. package/dist/review-meta.d.ts +65 -0
  124. package/dist/review-meta.js +149 -0
  125. package/dist/review.d.ts +35 -0
  126. package/dist/review.js +84 -0
  127. package/dist/runners/claude-code-sdk.d.ts +31 -0
  128. package/dist/runners/claude-code-sdk.js +238 -0
  129. package/dist/runners/claude-code.d.ts +3 -0
  130. package/dist/runners/claude-code.js +319 -73
  131. package/dist/runners/codex.js +4 -1
  132. package/dist/runners/copilot.js +4 -1
  133. package/dist/runners/cursor.js +4 -1
  134. package/dist/runners/git-utils.d.ts +81 -0
  135. package/dist/runners/git-utils.js +201 -0
  136. package/dist/runners/index.d.ts +5 -1
  137. package/dist/runners/index.js +4 -0
  138. package/dist/runners/review-agent.d.ts +67 -0
  139. package/dist/runners/review-agent.js +350 -0
  140. package/dist/runners/runner-registry.js +10 -0
  141. package/dist/runners/sdk-review-runner.d.ts +65 -0
  142. package/dist/runners/sdk-review-runner.js +185 -0
  143. package/dist/runners/security-triage.d.ts +20 -4
  144. package/dist/runners/security-triage.js +42 -14
  145. package/dist/runners/types.d.ts +25 -0
  146. package/dist/runtime/attestations.d.ts +274 -0
  147. package/dist/runtime/attestations.js +460 -0
  148. package/dist/runtime/git-env.d.ts +53 -0
  149. package/dist/runtime/git-env.js +60 -0
  150. package/dist/runtime/index.d.ts +7 -0
  151. package/dist/runtime/index.js +7 -0
  152. package/dist/runtime/parallelism-flag.d.ts +10 -0
  153. package/dist/runtime/parallelism-flag.js +18 -0
  154. package/dist/runtime/port-allocator.d.ts +21 -0
  155. package/dist/runtime/port-allocator.js +66 -0
  156. package/dist/runtime/worktree-pool.d.ts +86 -0
  157. package/dist/runtime/worktree-pool.js +204 -0
  158. package/dist/runtime/worktree.d.ts +25 -0
  159. package/dist/runtime/worktree.js +111 -0
  160. package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
  161. package/dist/sa-scoring/auto-calibrate.js +107 -0
  162. package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
  163. package/dist/sa-scoring/c1-sa2-computable.js +59 -0
  164. package/dist/sa-scoring/composite.d.ts +107 -0
  165. package/dist/sa-scoring/composite.js +139 -0
  166. package/dist/sa-scoring/depparse-client.d.ts +79 -0
  167. package/dist/sa-scoring/depparse-client.js +187 -0
  168. package/dist/sa-scoring/did-compiler.d.ts +122 -0
  169. package/dist/sa-scoring/did-compiler.js +286 -0
  170. package/dist/sa-scoring/drift-monitor.d.ts +84 -0
  171. package/dist/sa-scoring/drift-monitor.js +186 -0
  172. package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
  173. package/dist/sa-scoring/exemplar-bank.js +154 -0
  174. package/dist/sa-scoring/feedback-store.d.ts +100 -0
  175. package/dist/sa-scoring/feedback-store.js +156 -0
  176. package/dist/sa-scoring/index.d.ts +71 -0
  177. package/dist/sa-scoring/index.js +158 -0
  178. package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
  179. package/dist/sa-scoring/layer1-deterministic.js +298 -0
  180. package/dist/sa-scoring/layer2-structural.d.ts +71 -0
  181. package/dist/sa-scoring/layer2-structural.js +151 -0
  182. package/dist/sa-scoring/layer3-llm.d.ts +86 -0
  183. package/dist/sa-scoring/layer3-llm.js +282 -0
  184. package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
  185. package/dist/sa-scoring/rescore-orchestrator.js +47 -0
  186. package/dist/scheduling/burn-down.d.ts +27 -0
  187. package/dist/scheduling/burn-down.js +43 -0
  188. package/dist/scheduling/calibration.d.ts +42 -0
  189. package/dist/scheduling/calibration.js +150 -0
  190. package/dist/scheduling/index.d.ts +8 -0
  191. package/dist/scheduling/index.js +8 -0
  192. package/dist/scheduling/ledger.d.ts +59 -0
  193. package/dist/scheduling/ledger.js +216 -0
  194. package/dist/scheduling/off-peak.d.ts +27 -0
  195. package/dist/scheduling/off-peak.js +112 -0
  196. package/dist/scheduling/schedule-decision.d.ts +39 -0
  197. package/dist/scheduling/schedule-decision.js +89 -0
  198. package/dist/scheduling/tier-analysis.d.ts +47 -0
  199. package/dist/scheduling/tier-analysis.js +81 -0
  200. package/dist/scheduling/types.d.ts +140 -0
  201. package/dist/scheduling/types.js +11 -0
  202. package/dist/shared.d.ts +13 -0
  203. package/dist/shared.js +32 -0
  204. package/dist/state/index.d.ts +1 -1
  205. package/dist/state/schema.d.ts +6 -1
  206. package/dist/state/schema.js +276 -1
  207. package/dist/state/store.d.ts +71 -1
  208. package/dist/state/store.js +464 -7
  209. package/dist/state/types.d.ts +174 -0
  210. package/dist/types.d.ts +1 -1
  211. package/dist/validate-agent-output.js +4 -1
  212. package/dist/watch.js +6 -0
  213. package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
  214. package/dist/workflow-patterns/artifact-writer.js +34 -0
  215. package/dist/workflow-patterns/classifiers.d.ts +10 -0
  216. package/dist/workflow-patterns/classifiers.js +72 -0
  217. package/dist/workflow-patterns/detector.d.ts +27 -0
  218. package/dist/workflow-patterns/detector.js +186 -0
  219. package/dist/workflow-patterns/index.d.ts +8 -0
  220. package/dist/workflow-patterns/index.js +7 -0
  221. package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
  222. package/dist/workflow-patterns/proposal-generator.js +183 -0
  223. package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
  224. package/dist/workflow-patterns/telemetry-ingest.js +103 -0
  225. package/dist/workflow-patterns/types.d.ts +61 -0
  226. package/dist/workflow-patterns/types.js +11 -0
  227. package/package.json +2 -2
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Per-stage rolling token-estimate calibration per RFC §14.6 (Q11). Tracks the last 20
3
+ * invocations per stage with exponentially-weighted average; emits EstimateBootstrapped
4
+ * after first run replaces the cold-start default; emits EstimateVariance when observed
5
+ * deviates from estimated by > 50%.
6
+ */
7
+ import { type LedgerEvent, type TokenEstimate } from './types.js';
8
+ export declare const COLD_START_DEFAULT: TokenEstimate;
9
+ export interface CalibrationStoreDeps {
10
+ io?: {
11
+ read: (path: string) => Promise<string | null>;
12
+ write: (path: string, content: string) => Promise<void>;
13
+ };
14
+ }
15
+ export declare class CalibrationStore {
16
+ private state;
17
+ private readonly path;
18
+ private readonly io;
19
+ constructor(artifactsDir: string, deps?: CalibrationStoreDeps);
20
+ load(): Promise<void>;
21
+ /**
22
+ * Resolve the effective estimate for a stage at dispatch time. Frozen declarations
23
+ * pin the operator value; otherwise rolling supersedes; otherwise declared; otherwise
24
+ * cold-start default (with a MissingEstimate event for the caller to surface).
25
+ */
26
+ resolveEstimate(stage: string, declared: TokenEstimate | undefined): {
27
+ estimate: TokenEstimate;
28
+ events: LedgerEvent[];
29
+ };
30
+ /**
31
+ * Record actual consumption after a stage completes. Updates the rolling estimate;
32
+ * emits EstimateBootstrapped on first replacement of cold-start; emits EstimateVariance
33
+ * when actuals diverge from the previous declared estimate by > threshold.
34
+ */
35
+ record(stage: string, actual: {
36
+ input: number;
37
+ output: number;
38
+ }, declared: TokenEstimate | undefined): Promise<LedgerEvent[]>;
39
+ getRollingEstimate(stage: string): TokenEstimate | undefined;
40
+ private persist;
41
+ }
42
+ //# sourceMappingURL=calibration.d.ts.map
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Per-stage rolling token-estimate calibration per RFC §14.6 (Q11). Tracks the last 20
3
+ * invocations per stage with exponentially-weighted average; emits EstimateBootstrapped
4
+ * after first run replaces the cold-start default; emits EstimateVariance when observed
5
+ * deviates from estimated by > 50%.
6
+ */
7
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
8
+ import { dirname, join } from 'node:path';
9
+ import { COLD_START_DEFAULT_INPUT, COLD_START_DEFAULT_OUTPUT, ESTIMATE_VARIANCE_THRESHOLD, ROLLING_WINDOW_SIZE, } from './types.js';
10
+ export const COLD_START_DEFAULT = {
11
+ input: COLD_START_DEFAULT_INPUT,
12
+ output: COLD_START_DEFAULT_OUTPUT,
13
+ };
14
+ export class CalibrationStore {
15
+ state = { version: 1, stages: {} };
16
+ path;
17
+ io;
18
+ constructor(artifactsDir, deps = {}) {
19
+ this.path = join(artifactsDir, '_ledger', 'stage-estimates.json');
20
+ this.io = deps.io ?? {
21
+ read: async (p) => {
22
+ try {
23
+ return await readFile(p, 'utf8');
24
+ }
25
+ catch (err) {
26
+ if (err.code === 'ENOENT')
27
+ return null;
28
+ throw err;
29
+ }
30
+ },
31
+ write: async (p, c) => {
32
+ await mkdir(dirname(p), { recursive: true });
33
+ await writeFile(p, c, 'utf8');
34
+ },
35
+ };
36
+ }
37
+ async load() {
38
+ const raw = await this.io.read(this.path);
39
+ if (!raw)
40
+ return;
41
+ try {
42
+ this.state = JSON.parse(raw);
43
+ }
44
+ catch {
45
+ // Malformed; reset.
46
+ this.state = { version: 1, stages: {} };
47
+ }
48
+ }
49
+ /**
50
+ * Resolve the effective estimate for a stage at dispatch time. Frozen declarations
51
+ * pin the operator value; otherwise rolling supersedes; otherwise declared; otherwise
52
+ * cold-start default (with a MissingEstimate event for the caller to surface).
53
+ */
54
+ resolveEstimate(stage, declared) {
55
+ const events = [];
56
+ if (declared?.frozen) {
57
+ return { estimate: declared, events };
58
+ }
59
+ const history = this.state.stages[stage];
60
+ if (history?.rollingEstimate) {
61
+ return { estimate: history.rollingEstimate, events };
62
+ }
63
+ if (declared) {
64
+ return { estimate: declared, events };
65
+ }
66
+ events.push({ type: 'MissingEstimate', stage });
67
+ return { estimate: COLD_START_DEFAULT, events };
68
+ }
69
+ /**
70
+ * Record actual consumption after a stage completes. Updates the rolling estimate;
71
+ * emits EstimateBootstrapped on first replacement of cold-start; emits EstimateVariance
72
+ * when actuals diverge from the previous declared estimate by > threshold.
73
+ */
74
+ async record(stage, actual, declared) {
75
+ const events = [];
76
+ let history = this.state.stages[stage];
77
+ const isFirstRun = !history || history.samples.length === 0;
78
+ if (!history) {
79
+ history = { stage, samples: [] };
80
+ this.state.stages[stage] = history;
81
+ }
82
+ history.samples.push({ ...actual, at: new Date().toISOString() });
83
+ if (history.samples.length > ROLLING_WINDOW_SIZE) {
84
+ history.samples.shift();
85
+ }
86
+ const newRolling = exponentiallyWeightedAverage(history.samples);
87
+ const previousRolling = history.rollingEstimate;
88
+ history.rollingEstimate = newRolling;
89
+ if (isFirstRun && !declared) {
90
+ history.bootstrappedAt = new Date().toISOString();
91
+ events.push({
92
+ type: 'EstimateBootstrapped',
93
+ stage,
94
+ coldStartDefault: COLD_START_DEFAULT,
95
+ firstRunActual: { input: actual.input, output: actual.output },
96
+ newRollingEstimate: newRolling,
97
+ });
98
+ }
99
+ if (declared && !declared.frozen) {
100
+ const declaredTotal = declared.input + declared.output;
101
+ const actualTotal = actual.input + actual.output;
102
+ if (declaredTotal > 0) {
103
+ const ratio = actualTotal / declaredTotal;
104
+ if (Math.abs(ratio - 1) > ESTIMATE_VARIANCE_THRESHOLD) {
105
+ events.push({
106
+ type: 'EstimateVariance',
107
+ stage,
108
+ declared,
109
+ observed: { input: actual.input, output: actual.output },
110
+ ratio,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ void previousRolling; // reserved for future delta event.
116
+ await this.persist();
117
+ return events;
118
+ }
119
+ getRollingEstimate(stage) {
120
+ return this.state.stages[stage]?.rollingEstimate;
121
+ }
122
+ async persist() {
123
+ await this.io.write(this.path, JSON.stringify(this.state));
124
+ }
125
+ }
126
+ /**
127
+ * Exponentially-weighted average over the last N samples (newest weighted most).
128
+ * Decay 0.85 — gentle preference for recent observations without ignoring history.
129
+ */
130
+ function exponentiallyWeightedAverage(samples) {
131
+ if (samples.length === 0)
132
+ return COLD_START_DEFAULT;
133
+ const decay = 0.85;
134
+ let sumW = 0;
135
+ let sumI = 0;
136
+ let sumO = 0;
137
+ // Newest sample (last in array) gets weight 1.0; older samples get progressively less.
138
+ for (let i = samples.length - 1; i >= 0; i--) {
139
+ const age = samples.length - 1 - i;
140
+ const w = Math.pow(decay, age);
141
+ sumW += w;
142
+ sumI += samples[i].input * w;
143
+ sumO += samples[i].output * w;
144
+ }
145
+ return {
146
+ input: Math.round(sumI / sumW),
147
+ output: Math.round(sumO / sumW),
148
+ };
149
+ }
150
+ //# sourceMappingURL=calibration.js.map
@@ -0,0 +1,8 @@
1
+ export * from './types.js';
2
+ export { isOffPeakAt, nextOffPeakStart, ageInDays, freshnessLevel, type FreshnessLevel, } from './off-peak.js';
3
+ export { SubscriptionLedger, validateTenantShares, type LedgerDeps } from './ledger.js';
4
+ export { evaluateSchedule, type ScheduleDecision, type ScheduleEvaluationContext, } from './schedule-decision.js';
5
+ export { CalibrationStore, COLD_START_DEFAULT, type CalibrationStoreDeps } from './calibration.js';
6
+ export { buildBurnDownReport, type BurnDownInput } from './burn-down.js';
7
+ export { analyzeTier, PLAN_COSTS_USD, type Confidence, type ContentionEvent, type TierAnalysisInput, type TierAnalysisResult, } from './tier-analysis.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ export * from './types.js';
2
+ export { isOffPeakAt, nextOffPeakStart, ageInDays, freshnessLevel, } from './off-peak.js';
3
+ export { SubscriptionLedger, validateTenantShares } from './ledger.js';
4
+ export { evaluateSchedule, } from './schedule-decision.js';
5
+ export { CalibrationStore, COLD_START_DEFAULT } from './calibration.js';
6
+ export { buildBurnDownReport } from './burn-down.js';
7
+ export { analyzeTier, PLAN_COSTS_USD, } from './tier-analysis.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * SubscriptionLedger per RFC §14.2 / §14.12. Tracks per-(harness, accountId, tenant)
3
+ * window state derived from observed token consumption. Persists per-key state to
4
+ * $ARTIFACTS_DIR/_ledger/<harness>-<accountIdShort>-<tenant>.json so it survives
5
+ * orchestrator restarts within a window.
6
+ */
7
+ import { type AdmissionDecision, type LedgerKey, type SubscriptionPlan, type TokenEstimate, type WindowState } from './types.js';
8
+ export interface LedgerDeps {
9
+ now?: () => Date;
10
+ /** Override file IO for tests. */
11
+ io?: {
12
+ read: (path: string) => Promise<string | null>;
13
+ write: (path: string, content: string) => Promise<void>;
14
+ };
15
+ }
16
+ export declare class SubscriptionLedger {
17
+ private readonly state;
18
+ private readonly now;
19
+ private readonly io;
20
+ private readonly artifactsDir;
21
+ constructor(artifactsDir: string, deps?: LedgerDeps);
22
+ static keyToString(key: LedgerKey): string;
23
+ static keyToFilename(key: LedgerKey): string;
24
+ private filePathFor;
25
+ /** Load persisted state for a key (or initialize a fresh window). */
26
+ load(key: LedgerKey, plan: SubscriptionPlan): Promise<void>;
27
+ windowState(key: LedgerKey, plan: SubscriptionPlan): WindowState;
28
+ /**
29
+ * Decide whether a stage with the given estimated token cost may admit. Off-peak
30
+ * multiplier reduces the effective cost (2x off-peak → 0.5x quota consumption).
31
+ */
32
+ admit(key: LedgerKey, plan: SubscriptionPlan, estimate: TokenEstimate): AdmissionDecision;
33
+ record(key: LedgerKey, plan: SubscriptionPlan, actualTokens: {
34
+ input: number;
35
+ output: number;
36
+ }): Promise<void>;
37
+ isOffPeak(plan: SubscriptionPlan, when?: Date): boolean;
38
+ /** Reset the ledger for a key (used by tests + the LedgerReconciliation flow). */
39
+ reset(key: LedgerKey): void;
40
+ private persist;
41
+ }
42
+ /**
43
+ * Tenant-share validation per RFC §14.12. Returns null if valid; otherwise the failed
44
+ * (harness, accountId) groups with their share sums. The orchestrator emits
45
+ * TenantShareInvalid for each failure and refuses to start.
46
+ */
47
+ export declare function validateTenantShares(pipelines: ReadonlyArray<{
48
+ name: string;
49
+ harness: string;
50
+ accountId: string;
51
+ tenant?: string;
52
+ tenantQuotaShare?: number;
53
+ }>, tolerance?: number): Array<{
54
+ harness: string;
55
+ accountId: string;
56
+ sumOfShares: number;
57
+ pipelines: string[];
58
+ }>;
59
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1,216 @@
1
+ /**
2
+ * SubscriptionLedger per RFC §14.2 / §14.12. Tracks per-(harness, accountId, tenant)
3
+ * window state derived from observed token consumption. Persists per-key state to
4
+ * $ARTIFACTS_DIR/_ledger/<harness>-<accountIdShort>-<tenant>.json so it survives
5
+ * orchestrator restarts within a window.
6
+ */
7
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
8
+ import { dirname, join } from 'node:path';
9
+ import { isOffPeakAt } from './off-peak.js';
10
+ import { DEFAULT_TENANT, } from './types.js';
11
+ export class SubscriptionLedger {
12
+ state = new Map();
13
+ now;
14
+ io;
15
+ artifactsDir;
16
+ constructor(artifactsDir, deps = {}) {
17
+ this.artifactsDir = artifactsDir;
18
+ this.now = deps.now ?? (() => new Date());
19
+ this.io = deps.io ?? {
20
+ read: async (p) => {
21
+ try {
22
+ return await readFile(p, 'utf8');
23
+ }
24
+ catch (err) {
25
+ if (err.code === 'ENOENT')
26
+ return null;
27
+ throw err;
28
+ }
29
+ },
30
+ write: async (p, c) => {
31
+ await mkdir(dirname(p), { recursive: true });
32
+ await writeFile(p, c, 'utf8');
33
+ },
34
+ };
35
+ }
36
+ static keyToString(key) {
37
+ return `${key.harness}|${key.accountId}|${key.tenant ?? DEFAULT_TENANT}`;
38
+ }
39
+ static keyToFilename(key) {
40
+ const accountIdShort = key.accountId.slice(0, 8);
41
+ const tenant = key.tenant ?? DEFAULT_TENANT;
42
+ return `${key.harness}-${accountIdShort}-${tenant}.json`;
43
+ }
44
+ filePathFor(key) {
45
+ return join(this.artifactsDir, '_ledger', SubscriptionLedger.keyToFilename(key));
46
+ }
47
+ /** Load persisted state for a key (or initialize a fresh window). */
48
+ async load(key, plan) {
49
+ const path = this.filePathFor(key);
50
+ const raw = await this.io.read(path);
51
+ if (raw) {
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ const now = this.now();
55
+ const windowStart = new Date(parsed.windowStart);
56
+ const windowEnd = computeWindowEnd(windowStart, plan);
57
+ if (now.getTime() < windowEnd.getTime()) {
58
+ this.state.set(SubscriptionLedger.keyToString(key), parsed);
59
+ return;
60
+ }
61
+ // Window has expired; reset.
62
+ }
63
+ catch {
64
+ // Malformed; reset.
65
+ }
66
+ }
67
+ this.state.set(SubscriptionLedger.keyToString(key), {
68
+ windowStart: this.now().toISOString(),
69
+ consumedTokens: 0,
70
+ });
71
+ await this.persist(key);
72
+ }
73
+ windowState(key, plan) {
74
+ const persisted = this.state.get(SubscriptionLedger.keyToString(key));
75
+ if (!persisted)
76
+ throw new Error(`Ledger not loaded for key: ${SubscriptionLedger.keyToString(key)}`);
77
+ const windowStart = new Date(persisted.windowStart);
78
+ const windowEnd = computeWindowEnd(windowStart, plan);
79
+ const multiplier = plan.offPeak && isOffPeakAt(plan.offPeak, this.now()) ? plan.offPeak.multiplier : 1.0;
80
+ const quotaTokens = plan.windowQuotaTokens ?? Number.POSITIVE_INFINITY;
81
+ return {
82
+ windowStart,
83
+ windowEnd,
84
+ consumedTokens: persisted.consumedTokens,
85
+ quotaTokens,
86
+ multiplier,
87
+ utilizationFraction: quotaTokens === Number.POSITIVE_INFINITY ? 0 : persisted.consumedTokens / quotaTokens,
88
+ pacingTarget: plan.pacingTarget,
89
+ hardCap: plan.hardCap,
90
+ };
91
+ }
92
+ /**
93
+ * Decide whether a stage with the given estimated token cost may admit. Off-peak
94
+ * multiplier reduces the effective cost (2x off-peak → 0.5x quota consumption).
95
+ */
96
+ admit(key, plan, estimate) {
97
+ const ws = this.windowState(key, plan);
98
+ if (plan.billingMode === 'pay-per-token' || ws.quotaTokens === Number.POSITIVE_INFINITY) {
99
+ return { kind: 'yes', reason: 'pay-per-token plan; no quota' };
100
+ }
101
+ const totalEstimate = (estimate.input + estimate.output) / ws.multiplier;
102
+ const wouldConsume = ws.consumedTokens + totalEstimate;
103
+ const projectedFraction = wouldConsume / ws.quotaTokens;
104
+ if (projectedFraction <= plan.hardCap) {
105
+ return {
106
+ kind: 'yes',
107
+ reason: `under hardCap (${projectedFraction.toFixed(3)} ≤ ${plan.hardCap})`,
108
+ };
109
+ }
110
+ return {
111
+ kind: 'no',
112
+ reason: `would exceed hardCap (${projectedFraction.toFixed(3)} > ${plan.hardCap})`,
113
+ blockedBy: 'hardCap',
114
+ };
115
+ }
116
+ async record(key, plan, actualTokens) {
117
+ const persisted = this.state.get(SubscriptionLedger.keyToString(key));
118
+ if (!persisted)
119
+ throw new Error(`Ledger not loaded for key: ${SubscriptionLedger.keyToString(key)}`);
120
+ const multiplier = plan.offPeak && isOffPeakAt(plan.offPeak, this.now()) ? plan.offPeak.multiplier : 1.0;
121
+ persisted.consumedTokens += (actualTokens.input + actualTokens.output) / multiplier;
122
+ await this.persist(key);
123
+ }
124
+ isOffPeak(plan, when) {
125
+ if (!plan.offPeak)
126
+ return false;
127
+ return isOffPeakAt(plan.offPeak, when ?? this.now());
128
+ }
129
+ /** Reset the ledger for a key (used by tests + the LedgerReconciliation flow). */
130
+ reset(key) {
131
+ this.state.set(SubscriptionLedger.keyToString(key), {
132
+ windowStart: this.now().toISOString(),
133
+ consumedTokens: 0,
134
+ });
135
+ }
136
+ async persist(key) {
137
+ const persisted = this.state.get(SubscriptionLedger.keyToString(key));
138
+ if (!persisted)
139
+ return;
140
+ await this.io.write(this.filePathFor(key), JSON.stringify(persisted));
141
+ }
142
+ }
143
+ function computeWindowEnd(windowStart, plan) {
144
+ if (plan.billingMode === 'monthly-cap') {
145
+ // Roll at month boundary.
146
+ const next = new Date(windowStart);
147
+ next.setMonth(next.getMonth() + 1);
148
+ return next;
149
+ }
150
+ if (plan.billingMode === 'session-window' && plan.windowDuration) {
151
+ return new Date(windowStart.getTime() + parseIso8601DurationMs(plan.windowDuration));
152
+ }
153
+ // pay-per-token: no window.
154
+ return new Date(windowStart.getTime() + 365 * 24 * 60 * 60 * 1000);
155
+ }
156
+ /** Minimal ISO 8601 duration parser supporting PT<N>H / PT<N>M / P<N>D combinations. */
157
+ function parseIso8601DurationMs(duration) {
158
+ const m = duration.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?)?$/);
159
+ if (!m)
160
+ throw new Error(`Unsupported ISO 8601 duration: ${duration}`);
161
+ const days = Number.parseInt(m[1] ?? '0', 10);
162
+ const hours = Number.parseInt(m[2] ?? '0', 10);
163
+ const minutes = Number.parseInt(m[3] ?? '0', 10);
164
+ return ((days * 24 + hours) * 60 + minutes) * 60 * 1000;
165
+ }
166
+ /**
167
+ * Tenant-share validation per RFC §14.12. Returns null if valid; otherwise the failed
168
+ * (harness, accountId) groups with their share sums. The orchestrator emits
169
+ * TenantShareInvalid for each failure and refuses to start.
170
+ */
171
+ export function validateTenantShares(pipelines, tolerance = 0.001) {
172
+ const groups = new Map();
173
+ for (const p of pipelines) {
174
+ const key = `${p.harness}|${p.accountId}`;
175
+ let group = groups.get(key);
176
+ if (!group) {
177
+ group = {
178
+ harness: p.harness,
179
+ accountId: p.accountId,
180
+ pipelines: [],
181
+ shares: [],
182
+ tenants: new Set(),
183
+ };
184
+ groups.set(key, group);
185
+ }
186
+ group.pipelines.push(p.name);
187
+ group.shares.push(p.tenantQuotaShare);
188
+ if (p.tenant)
189
+ group.tenants.add(p.tenant);
190
+ }
191
+ const failures = [];
192
+ for (const group of groups.values()) {
193
+ if (group.tenants.size === 0)
194
+ continue; // all untenanted; no validation needed
195
+ if (group.shares.some((s) => s === undefined)) {
196
+ failures.push({
197
+ harness: group.harness,
198
+ accountId: group.accountId,
199
+ sumOfShares: NaN,
200
+ pipelines: group.pipelines,
201
+ });
202
+ continue;
203
+ }
204
+ const sum = group.shares.reduce((a, b) => a + b, 0);
205
+ if (Math.abs(sum - 1.0) > tolerance) {
206
+ failures.push({
207
+ harness: group.harness,
208
+ accountId: group.accountId,
209
+ sumOfShares: sum,
210
+ pipelines: group.pipelines,
211
+ });
212
+ }
213
+ }
214
+ return failures;
215
+ }
216
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Off-peak window evaluation per RFC §14.5. Operator-declared schedules with hour ranges
3
+ * (possibly wrapping midnight) and optional day-of-week filters, evaluated against an
4
+ * IANA timezone.
5
+ */
6
+ import type { OffPeakSchedule } from './types.js';
7
+ /**
8
+ * Returns true when the given instant falls inside any of the schedule's windows.
9
+ * Each window's `hours` is a range like '22-06' (which wraps midnight) or '0-7'.
10
+ */
11
+ export declare function isOffPeakAt(schedule: OffPeakSchedule, when: Date): boolean;
12
+ /**
13
+ * Returns the next off-peak window start strictly after `from`. Iterates by hour up to
14
+ * a 7-day horizon — bounded scan since schedules are weekly-periodic.
15
+ */
16
+ export declare function nextOffPeakStart(schedule: OffPeakSchedule, from: Date): Date | null;
17
+ /**
18
+ * Days since `lastVerified`. Returns Infinity if the date is missing/unparseable.
19
+ */
20
+ export declare function ageInDays(lastVerified: string | undefined, now?: Date): number;
21
+ export type FreshnessLevel = 'fresh' | 'advisory' | 'error';
22
+ /**
23
+ * Returns the freshness severity per RFC §14.5: fresh ≤ 30 days, advisory 30–90 days,
24
+ * error > 90 days (or missing). Operators see this in cli-status --subscriptions.
25
+ */
26
+ export declare function freshnessLevel(lastVerified: string | undefined, now?: Date): FreshnessLevel;
27
+ //# sourceMappingURL=off-peak.d.ts.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Off-peak window evaluation per RFC §14.5. Operator-declared schedules with hour ranges
3
+ * (possibly wrapping midnight) and optional day-of-week filters, evaluated against an
4
+ * IANA timezone.
5
+ */
6
+ const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
7
+ /**
8
+ * Returns true when the given instant falls inside any of the schedule's windows.
9
+ * Each window's `hours` is a range like '22-06' (which wraps midnight) or '0-7'.
10
+ */
11
+ export function isOffPeakAt(schedule, when) {
12
+ if (!schedule.enabled || schedule.schedule.length === 0)
13
+ return false;
14
+ for (const window of schedule.schedule) {
15
+ if (windowMatches(window, when))
16
+ return true;
17
+ }
18
+ return false;
19
+ }
20
+ function windowMatches(window, when) {
21
+ const hourMatch = window.hours.match(/^(\d{1,2})-(\d{1,2})$/);
22
+ if (!hourMatch)
23
+ return false;
24
+ const startHour = Number.parseInt(hourMatch[1], 10);
25
+ const endHour = Number.parseInt(hourMatch[2], 10);
26
+ // Get the local hour + day-of-week in the window's timezone.
27
+ const local = getLocalTimeInTimezone(when, window.tz);
28
+ if (!local)
29
+ return false;
30
+ // Day filter (optional).
31
+ if (window.daysOfWeek) {
32
+ const allowedDays = new Set(window.daysOfWeek.split(',').map((d) => d.trim()));
33
+ if (!allowedDays.has(DAY_NAMES[local.dayOfWeek]))
34
+ return false;
35
+ }
36
+ // Hour range; wraps midnight when start > end.
37
+ if (startHour <= endHour) {
38
+ return local.hour >= startHour && local.hour < endHour;
39
+ }
40
+ // Wrapping case: e.g., 22-06 means [22, 24) ∪ [0, 6).
41
+ return local.hour >= startHour || local.hour < endHour;
42
+ }
43
+ function getLocalTimeInTimezone(when, tz) {
44
+ try {
45
+ const fmt = new Intl.DateTimeFormat('en-US', {
46
+ timeZone: tz,
47
+ hour: 'numeric',
48
+ hourCycle: 'h23',
49
+ weekday: 'short',
50
+ });
51
+ const parts = fmt.formatToParts(when);
52
+ let hour = null;
53
+ let weekday = null;
54
+ for (const p of parts) {
55
+ if (p.type === 'hour')
56
+ hour = Number.parseInt(p.value, 10);
57
+ else if (p.type === 'weekday')
58
+ weekday = p.value;
59
+ }
60
+ if (hour === null || weekday === null)
61
+ return null;
62
+ const dayIndex = DAY_NAMES.indexOf(weekday);
63
+ if (dayIndex < 0)
64
+ return null;
65
+ return { hour, dayOfWeek: dayIndex };
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * Returns the next off-peak window start strictly after `from`. Iterates by hour up to
73
+ * a 7-day horizon — bounded scan since schedules are weekly-periodic.
74
+ */
75
+ export function nextOffPeakStart(schedule, from) {
76
+ if (!schedule.enabled || schedule.schedule.length === 0)
77
+ return null;
78
+ const HORIZON_HOURS = 24 * 7;
79
+ for (let h = 1; h <= HORIZON_HOURS; h++) {
80
+ const candidate = new Date(from.getTime() + h * 60 * 60 * 1000);
81
+ const candidateMinusHour = new Date(candidate.getTime() - 60 * 60 * 1000);
82
+ // Find a transition from off → on.
83
+ if (isOffPeakAt(schedule, candidate) && !isOffPeakAt(schedule, candidateMinusHour)) {
84
+ return candidate;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ /**
90
+ * Days since `lastVerified`. Returns Infinity if the date is missing/unparseable.
91
+ */
92
+ export function ageInDays(lastVerified, now = new Date()) {
93
+ if (!lastVerified)
94
+ return Infinity;
95
+ const parsed = new Date(lastVerified);
96
+ if (Number.isNaN(parsed.getTime()))
97
+ return Infinity;
98
+ return Math.floor((now.getTime() - parsed.getTime()) / (24 * 60 * 60 * 1000));
99
+ }
100
+ /**
101
+ * Returns the freshness severity per RFC §14.5: fresh ≤ 30 days, advisory 30–90 days,
102
+ * error > 90 days (or missing). Operators see this in cli-status --subscriptions.
103
+ */
104
+ export function freshnessLevel(lastVerified, now = new Date()) {
105
+ const age = ageInDays(lastVerified, now);
106
+ if (age <= 30)
107
+ return 'fresh';
108
+ if (age <= 90)
109
+ return 'advisory';
110
+ return 'error';
111
+ }
112
+ //# sourceMappingURL=off-peak.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Schedule-aware dispatch decision per RFC §14.3. Implements the four schedule modes
3
+ * (now, off-peak, quota-permitting, defer-if-low-priority) on top of the
4
+ * SubscriptionLedger admission decision.
5
+ */
6
+ import type { SubscriptionLedger } from './ledger.js';
7
+ import type { LedgerKey, Schedule, SubscriptionPlan, TokenEstimate } from './types.js';
8
+ export type ScheduleDecision = {
9
+ kind: 'dispatch-now';
10
+ reason: string;
11
+ } | {
12
+ kind: 'wait-until';
13
+ until: Date;
14
+ reason: string;
15
+ } | {
16
+ kind: 'requeue';
17
+ reason: string;
18
+ } | {
19
+ kind: 'denied';
20
+ reason: string;
21
+ blockedBy: string;
22
+ };
23
+ export interface ScheduleEvaluationContext {
24
+ ledger: SubscriptionLedger;
25
+ ledgerKey: LedgerKey;
26
+ plan: SubscriptionPlan;
27
+ estimate: TokenEstimate;
28
+ /**
29
+ * PPA score in [0,1]. Used by 'defer-if-low-priority' to decide whether to
30
+ * dispatch immediately (top quartile or > 30% headroom in the window).
31
+ */
32
+ ppaScore?: number;
33
+ queueScores?: number[];
34
+ /** Maximum time to wait for the next off-peak window. ISO 8601 duration. */
35
+ offPeakMaxWait?: string;
36
+ now?: () => Date;
37
+ }
38
+ export declare function evaluateSchedule(schedule: Schedule, ctx: ScheduleEvaluationContext): ScheduleDecision;
39
+ //# sourceMappingURL=schedule-decision.d.ts.map