@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
@@ -20,6 +20,139 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { parse as parseYaml } from 'yaml';
22
22
  import { normalizeBacklogPriority } from './admission-score.js';
23
+ // ── Code-area extraction ────────────────────────────────────────────
24
+ /**
25
+ * Extract a code-area string from a list of file/path references.
26
+ *
27
+ * Algorithm:
28
+ * 1. Filter references down to path-like strings (not URLs, not AISDLC-N
29
+ * IDs, not RFC-NNNN IDs) — the path-like entries are the ones that
30
+ * carry blast-radius signal.
31
+ * 2. Compute the deepest common path prefix across all filtered paths
32
+ * at the directory level (e.g. `pipeline-cli/src/orchestrator/filters/x.ts`
33
+ * + `pipeline-cli/src/orchestrator/loop.ts` → `pipeline-cli/src/orchestrator`).
34
+ * 3. Return the prefix, or `undefined` when there are no path references.
35
+ *
36
+ * The caller uses the result to populate `EnrichmentContext.codeArea` so
37
+ * `buildCodeAreaQuality()` can look up per-area metrics instead of the
38
+ * uniform Eρ variance of 0.30.
39
+ *
40
+ * @param references Raw reference strings from frontmatter `references:` AND
41
+ * any `## References` body section — callers are responsible
42
+ * for merging both sources before calling here.
43
+ */
44
+ export function extractCodeAreaFromReferences(references) {
45
+ // Strip leading/trailing backtick pairs, parentheses, brackets and whitespace
46
+ // (common in `## References` bullet items like `` `path/to/file.ts` ``).
47
+ const cleaned = references.map((r) => r
48
+ .replace(/^[`(['"]*/, '')
49
+ .replace(/[`)\]'"]*$/, '')
50
+ .trim());
51
+ // Keep only path-like references. Exclude:
52
+ // - URLs (http://, https://, ...)
53
+ // - AISDLC-N IDs
54
+ // - RFC-NNNN IDs
55
+ // - Plain words with no path separator
56
+ // - Empty strings
57
+ const pathRefs = cleaned.filter((r) => {
58
+ if (!r)
59
+ return false;
60
+ if (/^https?:\/\//i.test(r))
61
+ return false;
62
+ if (/^aisdlc-\d+$/i.test(r))
63
+ return false;
64
+ if (/^rfc-\d{4}/i.test(r))
65
+ return false;
66
+ // Must contain at least one path separator to be treated as a file path.
67
+ // Also accept paths that look like `dir/file.ext` with an extension.
68
+ return r.includes('/');
69
+ });
70
+ if (pathRefs.length === 0)
71
+ return undefined;
72
+ if (pathRefs.length === 1) {
73
+ return pathPrefix(pathRefs[0]);
74
+ }
75
+ // Compute the deepest common directory prefix.
76
+ const segments = pathRefs.map((r) => pathPrefix(r).split('/'));
77
+ let common = segments[0];
78
+ for (let i = 1; i < segments.length; i++) {
79
+ common = commonPrefix(common, segments[i]);
80
+ }
81
+ if (common.length === 0)
82
+ return undefined;
83
+ return common.join('/');
84
+ }
85
+ /**
86
+ * Return the directory portion of a path reference.
87
+ * `pipeline-cli/src/foo.ts` → `pipeline-cli/src`
88
+ * `pipeline-cli/src/orchestrator/` → `pipeline-cli/src/orchestrator`
89
+ * `pipeline-cli/src/orchestrator` → `pipeline-cli/src` (no trailing slash → treat as file)
90
+ */
91
+ function pathPrefix(ref) {
92
+ const stripped = ref.replace(/\/$/, ''); // remove trailing slash
93
+ const lastSlash = stripped.lastIndexOf('/');
94
+ // If the last segment looks like a file (has an extension) or the path has a
95
+ // trailing slash (already a directory reference), take the parent. Otherwise
96
+ // treat the whole thing as a directory.
97
+ const lastSegment = lastSlash >= 0 ? stripped.slice(lastSlash + 1) : stripped;
98
+ if (lastSlash < 0)
99
+ return stripped; // bare filename with no directory
100
+ // Has a dot in the last segment AND it's not a hidden dir like `.ai-sdlc` → file
101
+ if (lastSegment.includes('.') && !lastSegment.startsWith('.')) {
102
+ return stripped.slice(0, lastSlash);
103
+ }
104
+ return stripped; // already a directory path
105
+ }
106
+ /**
107
+ * Longest common prefix of two string arrays (segment-by-segment).
108
+ */
109
+ function commonPrefix(a, b) {
110
+ const result = [];
111
+ const len = Math.min(a.length, b.length);
112
+ for (let i = 0; i < len; i++) {
113
+ if (a[i] !== b[i])
114
+ break;
115
+ result.push(a[i]);
116
+ }
117
+ return result;
118
+ }
119
+ /**
120
+ * Parse file-path references from a `## References` markdown section.
121
+ *
122
+ * The body format is a bullet list where each item may be a bare path,
123
+ * a backtick-quoted path, a description with an inline path, or a mix.
124
+ * We extract the first backtick-quoted segment or the first slash-
125
+ * containing word from each bullet.
126
+ */
127
+ export function parseBodyReferences(body) {
128
+ // Split the body by `## ` headings (preserving each section in its entirety)
129
+ // then find the `## References` section specifically. This avoids regex
130
+ // backtracking issues with greedy vs lazy quantifiers when the body contains
131
+ // multiple `## ` headings.
132
+ const sections = body.split(/\n(?=##\s)/);
133
+ const section = sections.find((s) => /^##\s+References\b/i.test(s));
134
+ if (!section)
135
+ return [];
136
+ const out = [];
137
+ for (const line of section.split(/\r?\n/)) {
138
+ // Must be a bullet item.
139
+ if (!/^\s*-\s/.test(line))
140
+ continue;
141
+ const content = line.replace(/^\s*-\s+/, '');
142
+ // Prefer backtick-quoted path.
143
+ const btMatch = content.match(/`([^`]+)`/);
144
+ if (btMatch) {
145
+ out.push(btMatch[1]);
146
+ continue;
147
+ }
148
+ // Fall back to the first slash-containing token.
149
+ const tokens = content.split(/\s+/);
150
+ const pathToken = tokens.find((t) => t.includes('/'));
151
+ if (pathToken)
152
+ out.push(pathToken);
153
+ }
154
+ return out;
155
+ }
23
156
  // ── Parser ──────────────────────────────────────────────────────────
24
157
  /**
25
158
  * Parse a Backlog.md task markdown file into `BacklogTaskSnapshot`.
@@ -38,6 +171,10 @@ export function parseBacklogTask(content, sourcePath) {
38
171
  const dependencies = normaliseStringArray(fm.dependencies);
39
172
  const description = extractSection(content, 'Description');
40
173
  const acceptanceCriteria = extractAcceptanceCriteria(content);
174
+ // Strip frontmatter block before parsing body sections (the `---` block
175
+ // can contain `## References`-like content that would confuse body parsing).
176
+ const bodyOnly = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
177
+ const bodyReferences = parseBodyReferences(bodyOnly);
41
178
  return {
42
179
  id,
43
180
  numericId,
@@ -51,6 +188,7 @@ export function parseBacklogTask(content, sourcePath) {
51
188
  createdBy: fm.created_by ? String(fm.created_by).trim() : undefined,
52
189
  acceptanceCriteria,
53
190
  references,
191
+ bodyReferences,
54
192
  dependencies,
55
193
  sourcePath,
56
194
  };
@@ -308,6 +446,12 @@ export function mapBacklogTaskToAdmissionInput(snap, options = {}) {
308
446
  status: snap.status,
309
447
  },
310
448
  };
449
+ // ── Code-area extraction ─────────────────────────────────────────
450
+ // Merge frontmatter references + body references and compute the deepest
451
+ // common path prefix. The result is forwarded to EnrichmentContext.codeArea
452
+ // so admission scoring can use per-area Eρ variance instead of 0.30.
453
+ const allReferences = [...snap.references, ...snap.bodyReferences];
454
+ const codeArea = extractCodeAreaFromReferences(allReferences);
311
455
  const priorityInputOverrides = {};
312
456
  if (soulAlignment !== 0.5)
313
457
  priorityInputOverrides.soulAlignment = soulAlignment;
@@ -329,7 +473,7 @@ export function mapBacklogTaskToAdmissionInput(snap, options = {}) {
329
473
  priorityInputOverrides.defectRiskFactor = defectRiskFactor;
330
474
  if (qualityFlags.length > 0)
331
475
  priorityInputOverrides.qualityFlags = qualityFlags;
332
- return { input, priorityInputOverrides, qualityFlags };
476
+ return { input, priorityInputOverrides, qualityFlags, codeArea };
333
477
  }
334
478
  function buildAdmissionBody(snap, complexity) {
335
479
  const parts = [];
@@ -10,6 +10,12 @@
10
10
  *
11
11
  * Per v1.1 note: this adjusts the multiplicative Cκ term, NOT SA-2
12
12
  * directly. Per-dimension calibration lands in PPA v1.1 §17.
13
+ *
14
+ * RFC-0009 Phase 2.2 extension: `buildSoulCalibrationMatrix` aggregates
15
+ * Cκ coefficients per-soul × per-dimension (N×M cells). Feedback events
16
+ * tagged with a soul slug as their `category` and a SA dimension drive
17
+ * the matrix cells. Souls × dimensions with insufficient data are omitted
18
+ * from the matrix (callers fall back to the scalar coefficient or 1.0).
13
19
  */
14
20
  import type { SaDimension } from './state/types.js';
15
21
  import type { SAFeedbackStore } from './sa-scoring/feedback-store.js';
@@ -40,4 +46,79 @@ export interface BuildCategoryCoefficientsInput {
40
46
  * scalar fallback applies to them.
41
47
  */
42
48
  export declare function buildCategoryCoefficients(feedback: SAFeedbackStore, input?: BuildCategoryCoefficientsInput): Record<string, number>;
49
+ /**
50
+ * Input options for `buildSoulCalibrationMatrix`.
51
+ */
52
+ export interface BuildSoulCalibrationMatrixInput {
53
+ /**
54
+ * Soul slugs to include in the matrix (N axis).
55
+ * Feedback events whose `category` matches a slug are collected.
56
+ */
57
+ souls: readonly string[];
58
+ /**
59
+ * SA dimensions to include in the matrix (M axis).
60
+ * When absent, all SA dimensions are sampled.
61
+ */
62
+ dimensions?: readonly SaDimension[];
63
+ /** Trailing-window filter (ISO timestamp). */
64
+ since?: string;
65
+ /**
66
+ * Minimum feedback event count per (soul, dimension) cell before a
67
+ * coefficient is emitted. Cells below this threshold are omitted from
68
+ * the matrix — callers should fall back to 1.0 (neutral) for absent cells.
69
+ */
70
+ minSampleSize?: number;
71
+ }
72
+ /**
73
+ * The N×M Cκ calibration matrix for a tessellated platform.
74
+ *
75
+ * cells[soulSlug][dimension] = calibration coefficient in [0.7, 1.3]
76
+ *
77
+ * Cells omitted from the map have insufficient feedback data.
78
+ * Callers treat absent cells as 1.0 (neutral, no calibration adjustment).
79
+ */
80
+ export interface SoulCalibrationMatrix {
81
+ /**
82
+ * N×M map: `{ soulSlug: { saDimension: coefficient } }`.
83
+ * Only cells with sufficient data are present.
84
+ */
85
+ cells: Record<string, Record<string, number>>;
86
+ /**
87
+ * Souls (N axis) included in this matrix — equal to `input.souls`.
88
+ * Useful for distinguishing "soul has data but coefficient is neutral"
89
+ * from "soul was not included in the query".
90
+ */
91
+ souls: readonly string[];
92
+ /**
93
+ * SA dimensions (M axis) sampled — equal to `input.dimensions` when
94
+ * provided, or all dimensions found in the feedback window.
95
+ */
96
+ dimensions: readonly string[];
97
+ }
98
+ /**
99
+ * Aggregate Cκ calibration coefficients per-soul × per-dimension (N×M cells).
100
+ *
101
+ * Feedback events tagged with:
102
+ * - `category = <soul-slug>` (identifies which soul the feedback is for)
103
+ * - `dimension = <SA-1 | SA-2>` (SA dimension the feedback applies to)
104
+ *
105
+ * ...drive the per-cell coefficient using the same formula as
106
+ * `computeCalibrationCoefficient`.
107
+ *
108
+ * Usage in tessellated admission scoring (RFC-0009 §6):
109
+ * - Look up `matrix.cells[targetSoul][dimension]` for the effective Cκ
110
+ * - Fall back to 1.0 (neutral) when the cell is absent (insufficient data)
111
+ * - Cross-soul aggregate: apply `crossSoulScoringRule` over per-soul cells
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const matrix = buildSoulCalibrationMatrix(feedback, {
116
+ * souls: ['soul-a', 'soul-b', 'soul-c'],
117
+ * dimensions: ['SA-1', 'SA-2'],
118
+ * minSampleSize: 5,
119
+ * });
120
+ * const ckSoulA = matrix.cells['soul-a']?.['SA-1'] ?? 1.0;
121
+ * ```
122
+ */
123
+ export declare function buildSoulCalibrationMatrix(feedback: SAFeedbackStore, input: BuildSoulCalibrationMatrixInput): SoulCalibrationMatrix;
43
124
  //# sourceMappingURL=calibration.d.ts.map
@@ -10,6 +10,12 @@
10
10
  *
11
11
  * Per v1.1 note: this adjusts the multiplicative Cκ term, NOT SA-2
12
12
  * directly. Per-dimension calibration lands in PPA v1.1 §17.
13
+ *
14
+ * RFC-0009 Phase 2.2 extension: `buildSoulCalibrationMatrix` aggregates
15
+ * Cκ coefficients per-soul × per-dimension (N×M cells). Feedback events
16
+ * tagged with a soul slug as their `category` and a SA dimension drive
17
+ * the matrix cells. Souls × dimensions with insufficient data are omitted
18
+ * from the matrix (callers fall back to the scalar coefficient or 1.0).
13
19
  */
14
20
  export const CALIBRATION_MIN = 0.7;
15
21
  export const CALIBRATION_MAX = 1.3;
@@ -73,4 +79,74 @@ export function buildCategoryCoefficients(feedback, input = {}) {
73
79
  }
74
80
  return result;
75
81
  }
82
+ /** All recognized SA dimensions for default matrix columns. */
83
+ const ALL_SA_DIMENSIONS = ['SA-1', 'SA-2'];
84
+ /**
85
+ * Aggregate Cκ calibration coefficients per-soul × per-dimension (N×M cells).
86
+ *
87
+ * Feedback events tagged with:
88
+ * - `category = <soul-slug>` (identifies which soul the feedback is for)
89
+ * - `dimension = <SA-1 | SA-2>` (SA dimension the feedback applies to)
90
+ *
91
+ * ...drive the per-cell coefficient using the same formula as
92
+ * `computeCalibrationCoefficient`.
93
+ *
94
+ * Usage in tessellated admission scoring (RFC-0009 §6):
95
+ * - Look up `matrix.cells[targetSoul][dimension]` for the effective Cκ
96
+ * - Fall back to 1.0 (neutral) when the cell is absent (insufficient data)
97
+ * - Cross-soul aggregate: apply `crossSoulScoringRule` over per-soul cells
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const matrix = buildSoulCalibrationMatrix(feedback, {
102
+ * souls: ['soul-a', 'soul-b', 'soul-c'],
103
+ * dimensions: ['SA-1', 'SA-2'],
104
+ * minSampleSize: 5,
105
+ * });
106
+ * const ckSoulA = matrix.cells['soul-a']?.['SA-1'] ?? 1.0;
107
+ * ```
108
+ */
109
+ export function buildSoulCalibrationMatrix(feedback, input) {
110
+ const dims = input.dimensions ?? ALL_SA_DIMENSIONS;
111
+ const cells = {};
112
+ for (const soul of input.souls) {
113
+ const soulCells = {};
114
+ for (const dim of dims) {
115
+ const window = { dimension: dim, since: input.since };
116
+ const events = feedback.list(window).filter((e) => e.category === soul);
117
+ if (events.length === 0)
118
+ continue;
119
+ const bucket = {
120
+ accepts: 0,
121
+ dismisses: 0,
122
+ escalates: 0,
123
+ overrides: 0,
124
+ };
125
+ for (const e of events) {
126
+ switch (e.signal) {
127
+ case 'accept':
128
+ bucket.accepts++;
129
+ break;
130
+ case 'dismiss':
131
+ bucket.dismisses++;
132
+ break;
133
+ case 'escalate':
134
+ bucket.escalates++;
135
+ break;
136
+ case 'override':
137
+ bucket.overrides = (bucket.overrides ?? 0) + 1;
138
+ break;
139
+ }
140
+ }
141
+ const sampleSize = bucket.accepts + bucket.dismisses + bucket.escalates;
142
+ if (sampleSize < (input.minSampleSize ?? 1))
143
+ continue;
144
+ soulCells[dim] = computeCalibrationCoefficient(bucket);
145
+ }
146
+ if (Object.keys(soulCells).length > 0) {
147
+ cells[soul] = soulCells;
148
+ }
149
+ }
150
+ return { cells, souls: input.souls, dimensions: dims };
151
+ }
76
152
  //# sourceMappingURL=calibration.js.map
@@ -39,6 +39,31 @@ export interface DetectRemoteOptions {
39
39
  * Detect the GitHub-style org/repo from the project's git origin remote.
40
40
  * Returns FALLBACK with detected=false when no remote is configured or
41
41
  * when the URL cannot be parsed.
42
+ *
43
+ * The git invocation is hardened against two failure modes (AISDLC-104):
44
+ *
45
+ * 1. **cwd inheritance race under parallel test workers.** Every git
46
+ * command uses `git -C <cwd>` so the working dir is pinned at the
47
+ * git argv level rather than relying solely on `child_process`
48
+ * honouring the `cwd:` spawn option. Both should agree, but `git -C`
49
+ * is a git-internal contract independent of any subprocess cwd
50
+ * inheritance race that can happen when `process.chdir()` is
51
+ * interleaved with subprocess spawn under thread/fork pools.
52
+ *
53
+ * 2. **Host-repo origin bleed via parent-directory walk-up.** If `cwd`
54
+ * contains an invalid `.git` (e.g. an empty directory left by an
55
+ * init test setup) git normally walks UP looking for a real `.git`
56
+ * and can resolve to an ancestor repository — i.e. when run from
57
+ * inside the ai-sdlc-framework checkout the test would silently see
58
+ * the framework's own origin rather than the fallback. We defend by
59
+ * calling `git rev-parse --show-toplevel` first and confirming the
60
+ * reported toplevel matches `cwd` (after symlink resolution). When
61
+ * it doesn't, we treat the directory as not-a-repo and return the
62
+ * fallback rather than reporting the ancestor's remote. This was
63
+ * preferred over `GIT_CEILING_DIRECTORIES` because the ceiling-list
64
+ * semantics only block walking INTO the listed dirs, not up FROM
65
+ * them — empirically `GIT_CEILING_DIRECTORIES=<cwd>` did not stop
66
+ * git from finding a parent repo.
42
67
  */
43
68
  export declare function detectGitRemote(opts?: DetectRemoteOptions): RemoteInfo;
44
69
  /**
@@ -9,6 +9,7 @@
9
9
  * remote is configured (e.g. a brand-new local-only repo).
10
10
  */
11
11
  import { execSync } from 'node:child_process';
12
+ import { realpathSync } from 'node:fs';
12
13
  const FALLBACK = { org: 'your-org', repo: 'your-repo', detected: false };
13
14
  /**
14
15
  * Parse a single remote URL into org/repo. Supports:
@@ -50,13 +51,65 @@ export function parseRemoteUrl(url) {
50
51
  * Detect the GitHub-style org/repo from the project's git origin remote.
51
52
  * Returns FALLBACK with detected=false when no remote is configured or
52
53
  * when the URL cannot be parsed.
54
+ *
55
+ * The git invocation is hardened against two failure modes (AISDLC-104):
56
+ *
57
+ * 1. **cwd inheritance race under parallel test workers.** Every git
58
+ * command uses `git -C <cwd>` so the working dir is pinned at the
59
+ * git argv level rather than relying solely on `child_process`
60
+ * honouring the `cwd:` spawn option. Both should agree, but `git -C`
61
+ * is a git-internal contract independent of any subprocess cwd
62
+ * inheritance race that can happen when `process.chdir()` is
63
+ * interleaved with subprocess spawn under thread/fork pools.
64
+ *
65
+ * 2. **Host-repo origin bleed via parent-directory walk-up.** If `cwd`
66
+ * contains an invalid `.git` (e.g. an empty directory left by an
67
+ * init test setup) git normally walks UP looking for a real `.git`
68
+ * and can resolve to an ancestor repository — i.e. when run from
69
+ * inside the ai-sdlc-framework checkout the test would silently see
70
+ * the framework's own origin rather than the fallback. We defend by
71
+ * calling `git rev-parse --show-toplevel` first and confirming the
72
+ * reported toplevel matches `cwd` (after symlink resolution). When
73
+ * it doesn't, we treat the directory as not-a-repo and return the
74
+ * fallback rather than reporting the ancestor's remote. This was
75
+ * preferred over `GIT_CEILING_DIRECTORIES` because the ceiling-list
76
+ * semantics only block walking INTO the listed dirs, not up FROM
77
+ * them — empirically `GIT_CEILING_DIRECTORIES=<cwd>` did not stop
78
+ * git from finding a parent repo.
53
79
  */
54
80
  export function detectGitRemote(opts = {}) {
55
81
  const cwd = opts.cwd ?? process.cwd();
56
82
  const exec = opts.execImpl ?? defaultExec;
83
+ // Step 1: confirm cwd is a real git repo whose toplevel IS cwd.
84
+ // If `git rev-parse --show-toplevel` errors OR returns an ancestor,
85
+ // treat as not-a-repo and return FALLBACK. This is the host-repo
86
+ // bleed defense: an empty/invalid `.git/` in cwd causes git to walk
87
+ // UP to a parent repo, and `--show-toplevel` then reports the parent
88
+ // — comparing realpaths catches it.
89
+ let toplevel;
90
+ try {
91
+ toplevel = exec(`git -C ${shellQuote(cwd)} rev-parse --show-toplevel`, {
92
+ cwd,
93
+ encoding: 'utf-8',
94
+ stdio: ['ignore', 'pipe', 'ignore'],
95
+ }).trim();
96
+ }
97
+ catch {
98
+ return FALLBACK;
99
+ }
100
+ if (!sameDir(toplevel, cwd)) {
101
+ // git resolved to an ancestor repository — host-repo bleed. The
102
+ // operator is in a directory that isn't itself a real git root, so
103
+ // we deliberately do NOT report the ancestor's origin; emit
104
+ // FALLBACK so init prints the explicit "no git origin remote
105
+ // detected" message and substitutes `your-org`.
106
+ return FALLBACK;
107
+ }
108
+ // Step 2: ask for the origin URL. If unset (no remote configured)
109
+ // or unparseable, fall back.
57
110
  let url;
58
111
  try {
59
- url = exec('git remote get-url origin', {
112
+ url = exec(`git -C ${shellQuote(cwd)} remote get-url origin`, {
60
113
  cwd,
61
114
  encoding: 'utf-8',
62
115
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -68,6 +121,32 @@ export function detectGitRemote(opts = {}) {
68
121
  const parsed = parseRemoteUrl(url);
69
122
  return parsed ?? FALLBACK;
70
123
  }
124
+ /**
125
+ * Compare two filesystem paths after symlink + canonicalization to
126
+ * decide whether they refer to the same directory. macOS aliases /tmp
127
+ * to /private/tmp, so a string compare of `cwd` against the toplevel
128
+ * git reports would otherwise fail spuriously. Falls back to literal
129
+ * compare when realpath isn't available (deleted dir, permission).
130
+ */
131
+ function sameDir(a, b) {
132
+ const norm = (p) => {
133
+ try {
134
+ return realpathSync(p);
135
+ }
136
+ catch {
137
+ return p;
138
+ }
139
+ };
140
+ return norm(a) === norm(b);
141
+ }
142
+ /**
143
+ * Quote a path for safe single-token interpolation into a shell command.
144
+ * Wraps in single quotes and escapes any embedded single quotes by
145
+ * closing the quote, emitting an escaped quote, then reopening.
146
+ */
147
+ function shellQuote(s) {
148
+ return `'${s.replace(/'/g, `'\\''`)}'`;
149
+ }
71
150
  function defaultExec(cmd, opts) {
72
151
  return execSync(cmd, {
73
152
  cwd: opts.cwd,