@ai-sdlc/orchestrator 0.10.0 → 0.14.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 (168) hide show
  1. package/dist/adapters.d.ts +42 -3
  2. package/dist/adapters.js +133 -3
  3. package/dist/admission-composite.d.ts +112 -1
  4. package/dist/admission-composite.js +85 -4
  5. package/dist/admission-enrichment.d.ts +12 -3
  6. package/dist/admission-enrichment.js +64 -13
  7. package/dist/admission-score.d.ts +30 -0
  8. package/dist/admission-score.js +4 -1
  9. package/dist/analysis/file-walker.js +5 -0
  10. package/dist/backlog-adapter.d.ts +45 -0
  11. package/dist/backlog-adapter.js +145 -1
  12. package/dist/calibration.d.ts +81 -0
  13. package/dist/calibration.js +76 -0
  14. package/dist/cli/commands/git-remote.js +1 -1
  15. package/dist/cli/commands/init-features.d.ts +301 -2
  16. package/dist/cli/commands/init-features.js +647 -7
  17. package/dist/cli/commands/init-templates.d.ts +198 -1
  18. package/dist/cli/commands/init-templates.js +943 -1
  19. package/dist/cli/commands/init.d.ts +45 -0
  20. package/dist/cli/commands/init.js +147 -5
  21. package/dist/cli/commands/run.js +9 -1
  22. package/dist/cli/index.d.ts +11 -0
  23. package/dist/cli/index.js +42 -8
  24. package/dist/compliance/composer.d.ts +79 -0
  25. package/dist/compliance/composer.js +258 -0
  26. package/dist/compliance/errors.d.ts +64 -0
  27. package/dist/compliance/errors.js +85 -0
  28. package/dist/compliance/loader.d.ts +52 -0
  29. package/dist/compliance/loader.js +124 -0
  30. package/dist/compliance/types.d.ts +184 -0
  31. package/dist/compliance/types.js +41 -0
  32. package/dist/compliance-clearance.d.ts +269 -0
  33. package/dist/compliance-clearance.js +269 -0
  34. package/dist/config.js +17 -0
  35. package/dist/cost-tracker.d.ts +22 -0
  36. package/dist/cost-tracker.js +41 -0
  37. package/dist/cycle-utils.js +14 -1
  38. package/dist/database/adapters/external.js +5 -1
  39. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  40. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  41. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  42. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  43. package/dist/embedding/cross-provider.d.ts +78 -0
  44. package/dist/embedding/cross-provider.js +75 -0
  45. package/dist/embedding/deprecation.d.ts +151 -0
  46. package/dist/embedding/deprecation.js +229 -0
  47. package/dist/embedding/errors.d.ts +90 -0
  48. package/dist/embedding/errors.js +150 -0
  49. package/dist/embedding/index.d.ts +29 -0
  50. package/dist/embedding/index.js +24 -0
  51. package/dist/embedding/pipeline-load.d.ts +146 -0
  52. package/dist/embedding/pipeline-load.js +178 -0
  53. package/dist/embedding/registry.d.ts +45 -0
  54. package/dist/embedding/registry.js +61 -0
  55. package/dist/embedding/stale-vector.d.ts +110 -0
  56. package/dist/embedding/stale-vector.js +92 -0
  57. package/dist/embedding/storage/index.d.ts +51 -0
  58. package/dist/embedding/storage/index.js +43 -0
  59. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  60. package/dist/embedding/storage/jsonl-backend.js +332 -0
  61. package/dist/embedding/storage/types.d.ts +135 -0
  62. package/dist/embedding/storage/types.js +13 -0
  63. package/dist/embedding/types.d.ts +180 -0
  64. package/dist/embedding/types.js +10 -0
  65. package/dist/execute.d.ts +29 -2
  66. package/dist/execute.js +200 -38
  67. package/dist/fix-review.js +1 -1
  68. package/dist/index.d.ts +14 -2
  69. package/dist/index.js +19 -2
  70. package/dist/journey/inheritance-validator.d.ts +396 -0
  71. package/dist/journey/inheritance-validator.js +370 -0
  72. package/dist/journey/metric-snapshot.d.ts +403 -0
  73. package/dist/journey/metric-snapshot.js +370 -0
  74. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  75. package/dist/journey/state-id-drift-rule.js +245 -0
  76. package/dist/journey-sa2-router.d.ts +395 -0
  77. package/dist/journey-sa2-router.js +308 -0
  78. package/dist/otel-exporter.js +0 -3
  79. package/dist/runners/claude-code.js +3 -3
  80. package/dist/runners/review-agent.js +6 -2
  81. package/dist/runners/runner-registry.d.ts +36 -0
  82. package/dist/runners/runner-registry.js +90 -0
  83. package/dist/runtime/attestations.d.ts +173 -13
  84. package/dist/runtime/attestations.js +265 -43
  85. package/dist/runtime/index.d.ts +1 -1
  86. package/dist/runtime/index.js +1 -1
  87. package/dist/sa-scoring/exemplar-bank.js +3 -1
  88. package/dist/sa-scoring/layer3-llm.js +6 -1
  89. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  90. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  91. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  92. package/dist/sa-scoring/revision-proposal.js +417 -0
  93. package/dist/shared.d.ts +28 -0
  94. package/dist/shared.js +37 -0
  95. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  96. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  97. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  98. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  99. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  100. package/dist/signal-ingestion/adapters/manual.js +112 -0
  101. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  102. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  103. package/dist/signal-ingestion/classifier.d.ts +205 -0
  104. package/dist/signal-ingestion/classifier.js +494 -0
  105. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  106. package/dist/signal-ingestion/clustering-types.js +14 -0
  107. package/dist/signal-ingestion/clustering.d.ts +200 -0
  108. package/dist/signal-ingestion/clustering.js +413 -0
  109. package/dist/signal-ingestion/config.d.ts +351 -0
  110. package/dist/signal-ingestion/config.js +587 -0
  111. package/dist/signal-ingestion/d1.d.ts +252 -0
  112. package/dist/signal-ingestion/d1.js +235 -0
  113. package/dist/signal-ingestion/errors.d.ts +73 -0
  114. package/dist/signal-ingestion/errors.js +108 -0
  115. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  116. package/dist/signal-ingestion/governance-events.js +189 -0
  117. package/dist/signal-ingestion/index.d.ts +35 -0
  118. package/dist/signal-ingestion/index.js +53 -0
  119. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  120. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  121. package/dist/signal-ingestion/registry.d.ts +40 -0
  122. package/dist/signal-ingestion/registry.js +137 -0
  123. package/dist/signal-ingestion/residency.d.ts +227 -0
  124. package/dist/signal-ingestion/residency.js +238 -0
  125. package/dist/signal-ingestion/significance.d.ts +554 -0
  126. package/dist/signal-ingestion/significance.js +555 -0
  127. package/dist/signal-ingestion/types.d.ts +191 -0
  128. package/dist/signal-ingestion/types.js +8 -0
  129. package/dist/substrate/drift-composition.d.ts +270 -0
  130. package/dist/substrate/drift-composition.js +306 -0
  131. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  132. package/dist/substrate/drift-tui-surface.js +102 -0
  133. package/dist/substrate/identity-class.d.ts +176 -0
  134. package/dist/substrate/identity-class.js +201 -0
  135. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  136. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  137. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  138. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  139. package/dist/tessellation/rule-registry.d.ts +269 -0
  140. package/dist/tessellation/rule-registry.js +92 -0
  141. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  142. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  143. package/dist/tessellation-admission.d.ts +162 -0
  144. package/dist/tessellation-admission.js +146 -0
  145. package/dist/tessellation-drift.d.ts +246 -0
  146. package/dist/tessellation-drift.js +250 -0
  147. package/dist/validate-config.js +13 -0
  148. package/dist/validate-issue.js +2 -2
  149. package/dist/variant/cardinality-activation.d.ts +126 -0
  150. package/dist/variant/cardinality-activation.js +101 -0
  151. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  152. package/dist/variant/deprecation-lifecycle.js +208 -0
  153. package/dist/variant/drift-extension.d.ts +136 -0
  154. package/dist/variant/drift-extension.js +164 -0
  155. package/dist/variant/engineering-review.d.ts +185 -0
  156. package/dist/variant/engineering-review.js +142 -0
  157. package/dist/variant/index.d.ts +32 -0
  158. package/dist/variant/index.js +32 -0
  159. package/dist/variant/inheritance-validator.d.ts +165 -0
  160. package/dist/variant/inheritance-validator.js +139 -0
  161. package/dist/variant/internal-adopter/index.d.ts +11 -0
  162. package/dist/variant/internal-adopter/index.js +10 -0
  163. package/dist/variant/internal-adopter/products.d.ts +156 -0
  164. package/dist/variant/internal-adopter/products.js +366 -0
  165. package/dist/variant-admission.d.ts +316 -0
  166. package/dist/variant-admission.js +247 -0
  167. package/dist/webhook-manager.js +7 -1
  168. package/package.json +15 -12
@@ -25,6 +25,18 @@ export interface AdmissionInput {
25
25
  commentCount: number;
26
26
  /** ISO timestamp of issue creation. */
27
27
  createdAt: string;
28
+ /**
29
+ * Canonical work item ID for dep-graph lookups (RFC-0014 / RFC-0009 Phase 2.1).
30
+ *
31
+ * When present, used instead of `#${issueNumber}` to match the work item
32
+ * against dep-graph snapshot entries (which use backlog task IDs like
33
+ * `AISDLC-313`). Backlog adapters set this from the task's frontmatter `id`.
34
+ *
35
+ * Absent for GitHub-issue based inputs — the admission composite falls back
36
+ * to `#${issueNumber}` which is consistent with `mapIssueToPriorityInput`'s
37
+ * `itemId` field.
38
+ */
39
+ workItemId?: string;
28
40
  /** GitHub author_association — determines trust-based signal boosting. */
29
41
  authorAssociation?: AuthorAssociation;
30
42
  /** GitHub login of the issue author (used by C5 principal match). */
@@ -116,6 +128,24 @@ export interface DesignAuthoritySignal {
116
128
  * RFC's §14.2 weighting constraint.
117
129
  */
118
130
  principalsDeclared?: boolean;
131
+ /**
132
+ * RFC-0008 §C5 Source 3 — automated compliance-assessment signal in [-1, 1].
133
+ *
134
+ * Populated by `buildDesignAuthoritySignal` when the resolved DSB carries
135
+ * `status.tokenCompliance.currentCoverage`:
136
+ * >= 0.8 (80 %) → small positive signal (+0.3): design system is healthy here
137
+ * < 0.4 (40 %) → small negative signal (-0.2): design system is fragile here
138
+ * otherwise → 0 (neutral)
139
+ *
140
+ * This signal fires from the DSB health data alone — no principal participation
141
+ * required. It is additive to the principal-participation weight in
142
+ * `computeDesignAuthorityWeight`, so a fully-loaded DSB with high compliance
143
+ * produces `hcDesign > 0` even when no design-authority principal commented.
144
+ *
145
+ * Absent (undefined) when no DSB is resolved or the DSB lacks token-compliance
146
+ * status — callers treat absence the same as 0.
147
+ */
148
+ complianceSignal?: number;
119
149
  }
120
150
  export interface AdmissionThresholds {
121
151
  minimumScore: number;
@@ -45,7 +45,10 @@ export function mapIssueToPriorityInput(input) {
45
45
  const isTrusted = assoc === 'OWNER' || assoc === 'MEMBER' || assoc === 'COLLABORATOR';
46
46
  const isContributor = assoc === 'CONTRIBUTOR';
47
47
  // ── Complexity from issue body, AC count, or backlog context ─
48
- const complexityMatch = input.body?.match(/###?\s*Complexity\s*\n+\s*(\d+)/i);
48
+ // Horizontal-whitespace classes ([ \t]) around \n+ so the sub-patterns can't
49
+ // overlap (\s includes \n) — avoids polynomial backtracking on attacker-
50
+ // controlled issue bodies (CodeQL js/polynomial-redos).
51
+ const complexityMatch = input.body?.match(/###?[ \t]*Complexity[ \t]*\n+[ \t]*(\d+)/i);
49
52
  let complexity = complexityMatch ? Number(complexityMatch[1]) : undefined;
50
53
  // Backlog tasks rarely include a `### Complexity` header — fall back to AC count
51
54
  // (each AC is roughly one logical unit; clamp at 10).
@@ -28,7 +28,12 @@ const MODULE_MARKERS = ['index.ts', 'index.js', 'index.mjs', 'package.json'];
28
28
  function matchesGlob(filePath, patterns) {
29
29
  for (const pattern of patterns) {
30
30
  // Simple glob matching: supports ** and *
31
+ // Escape backslashes FIRST before any other replacement so that the
32
+ // subsequent `.replace(/\./g, '\\.')` doesn't produce `\\.` sequences
33
+ // that are themselves broken when the input contained a `\`
34
+ // (CodeQL js/incomplete-sanitization alert #67).
31
35
  const regex = pattern
36
+ .replace(/\\/g, '\\\\')
32
37
  .replace(/\./g, '\\.')
33
38
  .replace(/\*\*/g, '{{GLOBSTAR}}')
34
39
  .replace(/\*/g, '[^/]*')
@@ -18,6 +18,36 @@
18
18
  */
19
19
  import type { PriorityInput, QualityFlag } from '@ai-sdlc/reference';
20
20
  import { type AdmissionInput } from './admission-score.js';
21
+ /**
22
+ * Extract a code-area string from a list of file/path references.
23
+ *
24
+ * Algorithm:
25
+ * 1. Filter references down to path-like strings (not URLs, not AISDLC-N
26
+ * IDs, not RFC-NNNN IDs) — the path-like entries are the ones that
27
+ * carry blast-radius signal.
28
+ * 2. Compute the deepest common path prefix across all filtered paths
29
+ * at the directory level (e.g. `pipeline-cli/src/orchestrator/filters/x.ts`
30
+ * + `pipeline-cli/src/orchestrator/loop.ts` → `pipeline-cli/src/orchestrator`).
31
+ * 3. Return the prefix, or `undefined` when there are no path references.
32
+ *
33
+ * The caller uses the result to populate `EnrichmentContext.codeArea` so
34
+ * `buildCodeAreaQuality()` can look up per-area metrics instead of the
35
+ * uniform Eρ variance of 0.30.
36
+ *
37
+ * @param references Raw reference strings from frontmatter `references:` AND
38
+ * any `## References` body section — callers are responsible
39
+ * for merging both sources before calling here.
40
+ */
41
+ export declare function extractCodeAreaFromReferences(references: string[]): string | undefined;
42
+ /**
43
+ * Parse file-path references from a `## References` markdown section.
44
+ *
45
+ * The body format is a bullet list where each item may be a bare path,
46
+ * a backtick-quoted path, a description with an inline path, or a mix.
47
+ * We extract the first backtick-quoted segment or the first slash-
48
+ * containing word from each bullet.
49
+ */
50
+ export declare function parseBodyReferences(body: string): string[];
21
51
  export interface BacklogAcceptanceCriterion {
22
52
  index: number;
23
53
  text: string;
@@ -38,6 +68,11 @@ export interface BacklogTaskSnapshot {
38
68
  createdBy?: string;
39
69
  acceptanceCriteria: BacklogAcceptanceCriterion[];
40
70
  references: string[];
71
+ /**
72
+ * References extracted from the `## References` body section.
73
+ * Merged with frontmatter `references` when computing `codeArea`.
74
+ */
75
+ bodyReferences: string[];
41
76
  /** Task IDs this task is blocked by (frontmatter `dependencies`). */
42
77
  dependencies: string[];
43
78
  /** Filesystem path the snapshot was read from. */
@@ -83,6 +118,16 @@ export interface BacklogAdmissionMapping {
83
118
  priorityInputOverrides: Partial<PriorityInput>;
84
119
  /** Quality flags surfaced for renderers (zombie close, etc). */
85
120
  qualityFlags: QualityFlag[];
121
+ /**
122
+ * Deepest common path prefix computed from all references in the task
123
+ * (frontmatter `references:` + `## References` body section).
124
+ *
125
+ * Pass to `EnrichmentContext.codeArea` when calling `enrichAdmissionInput()`
126
+ * so `buildCodeAreaQuality()` can look up per-area metrics instead of the
127
+ * uniform Eρ variance of 0.30. Undefined when the task has no file-path
128
+ * references — callers should fall through to the uniform default.
129
+ */
130
+ codeArea: string | undefined;
86
131
  }
87
132
  /**
88
133
  * Map a Backlog snapshot onto an `AdmissionInput` plus the
@@ -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
@@ -29,7 +29,7 @@ export function parseRemoteUrl(url) {
29
29
  return { org: sshShort[1].split('/').slice(-1)[0], repo: sshShort[2], detected: true };
30
30
  }
31
31
  // SSH or HTTPS with a scheme
32
- let parsed = null;
32
+ let parsed;
33
33
  try {
34
34
  parsed = new URL(trimmed);
35
35
  }