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