@oscharko-dev/keiko-quality-intelligence 0.2.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/.tsbuildinfo +1 -0
- package/dist/__tests__/_fixtureLoader.d.ts +9 -0
- package/dist/__tests__/_fixtureLoader.d.ts.map +1 -0
- package/dist/__tests__/_fixtureLoader.js +75 -0
- package/dist/domain/assertions.d.ts +61 -0
- package/dist/domain/assertions.d.ts.map +1 -0
- package/dist/domain/assertions.js +134 -0
- package/dist/domain/coverageRelevance.d.ts +73 -0
- package/dist/domain/coverageRelevance.d.ts.map +1 -0
- package/dist/domain/coverageRelevance.js +155 -0
- package/dist/domain/deduplication.d.ts +17 -0
- package/dist/domain/deduplication.d.ts.map +1 -0
- package/dist/domain/deduplication.js +95 -0
- package/dist/domain/figma/a11yBaseline.d.ts +17 -0
- package/dist/domain/figma/a11yBaseline.d.ts.map +1 -0
- package/dist/domain/figma/a11yBaseline.js +218 -0
- package/dist/domain/figma/cleanToScreenIr.d.ts +3 -0
- package/dist/domain/figma/cleanToScreenIr.d.ts.map +1 -0
- package/dist/domain/figma/cleanToScreenIr.js +62 -0
- package/dist/domain/figma/codeTargetAdapter.d.ts +30 -0
- package/dist/domain/figma/codeTargetAdapter.d.ts.map +1 -0
- package/dist/domain/figma/codeTargetAdapter.js +27 -0
- package/dist/domain/figma/color.d.ts +31 -0
- package/dist/domain/figma/color.d.ts.map +1 -0
- package/dist/domain/figma/color.js +99 -0
- package/dist/domain/figma/emissionPlan.d.ts +56 -0
- package/dist/domain/figma/emissionPlan.d.ts.map +1 -0
- package/dist/domain/figma/emissionPlan.js +87 -0
- package/dist/domain/figma/htmlCssAdapter.d.ts +13 -0
- package/dist/domain/figma/htmlCssAdapter.d.ts.map +1 -0
- package/dist/domain/figma/htmlCssAdapter.js +452 -0
- package/dist/domain/figma/index.d.ts +19 -0
- package/dist/domain/figma/index.d.ts.map +1 -0
- package/dist/domain/figma/index.js +31 -0
- package/dist/domain/figma/irTypes.d.ts +156 -0
- package/dist/domain/figma/irTypes.d.ts.map +1 -0
- package/dist/domain/figma/irTypes.js +8 -0
- package/dist/domain/figma/links.d.ts +6 -0
- package/dist/domain/figma/links.d.ts.map +1 -0
- package/dist/domain/figma/links.js +102 -0
- package/dist/domain/figma/navGraph.d.ts +74 -0
- package/dist/domain/figma/navGraph.d.ts.map +1 -0
- package/dist/domain/figma/navGraph.js +315 -0
- package/dist/domain/figma/normalize.d.ts +7 -0
- package/dist/domain/figma/normalize.d.ts.map +1 -0
- package/dist/domain/figma/normalize.js +252 -0
- package/dist/domain/figma/prune.d.ts +15 -0
- package/dist/domain/figma/prune.d.ts.map +1 -0
- package/dist/domain/figma/prune.js +65 -0
- package/dist/domain/figma/screenDetect.d.ts +8 -0
- package/dist/domain/figma/screenDetect.d.ts.map +1 -0
- package/dist/domain/figma/screenDetect.js +35 -0
- package/dist/domain/figma/screenIrTestBaseline.d.ts +52 -0
- package/dist/domain/figma/screenIrTestBaseline.d.ts.map +1 -0
- package/dist/domain/figma/screenIrTestBaseline.js +326 -0
- package/dist/domain/figma/semanticNaming.d.ts +24 -0
- package/dist/domain/figma/semanticNaming.d.ts.map +1 -0
- package/dist/domain/figma/semanticNaming.js +67 -0
- package/dist/domain/figma/sourceNode.d.ts +24 -0
- package/dist/domain/figma/sourceNode.d.ts.map +1 -0
- package/dist/domain/figma/sourceNode.js +26 -0
- package/dist/domain/figma/tokens.d.ts +11 -0
- package/dist/domain/figma/tokens.d.ts.map +1 -0
- package/dist/domain/figma/tokens.js +148 -0
- package/dist/domain/figma/visionAugmentation.d.ts +14 -0
- package/dist/domain/figma/visionAugmentation.d.ts.map +1 -0
- package/dist/domain/figma/visionAugmentation.js +48 -0
- package/dist/domain/intentDerivation.d.ts +21 -0
- package/dist/domain/intentDerivation.d.ts.map +1 -0
- package/dist/domain/intentDerivation.js +126 -0
- package/dist/domain/policyProfile.d.ts +37 -0
- package/dist/domain/policyProfile.d.ts.map +1 -0
- package/dist/domain/policyProfile.js +94 -0
- package/dist/domain/requirementExcerpt.d.ts +9 -0
- package/dist/domain/requirementExcerpt.d.ts.map +1 -0
- package/dist/domain/requirementExcerpt.js +39 -0
- package/dist/domain/staleness.d.ts +56 -0
- package/dist/domain/staleness.d.ts.map +1 -0
- package/dist/domain/staleness.js +313 -0
- package/dist/domain/testDesignModel.d.ts +38 -0
- package/dist/domain/testDesignModel.d.ts.map +1 -0
- package/dist/domain/testDesignModel.js +264 -0
- package/dist/domain/testQualityRubric.d.ts +20 -0
- package/dist/domain/testQualityRubric.d.ts.map +1 -0
- package/dist/domain/testQualityRubric.js +38 -0
- package/dist/domain/validation.d.ts +7 -0
- package/dist/domain/validation.d.ts.map +1 -0
- package/dist/domain/validation.js +145 -0
- package/dist/export/adapters/alm.d.ts +4 -0
- package/dist/export/adapters/alm.d.ts.map +1 -0
- package/dist/export/adapters/alm.js +75 -0
- package/dist/export/adapters/csv.d.ts +5 -0
- package/dist/export/adapters/csv.d.ts.map +1 -0
- package/dist/export/adapters/csv.js +55 -0
- package/dist/export/adapters/index.d.ts +13 -0
- package/dist/export/adapters/index.d.ts.map +1 -0
- package/dist/export/adapters/index.js +15 -0
- package/dist/export/adapters/jira.d.ts +5 -0
- package/dist/export/adapters/jira.d.ts.map +1 -0
- package/dist/export/adapters/jira.js +79 -0
- package/dist/export/adapters/json.d.ts +3 -0
- package/dist/export/adapters/json.d.ts.map +1 -0
- package/dist/export/adapters/json.js +54 -0
- package/dist/export/adapters/markdown.d.ts +3 -0
- package/dist/export/adapters/markdown.d.ts.map +1 -0
- package/dist/export/adapters/markdown.js +88 -0
- package/dist/export/adapters/plaintext.d.ts +3 -0
- package/dist/export/adapters/plaintext.d.ts.map +1 -0
- package/dist/export/adapters/plaintext.js +65 -0
- package/dist/export/adapters/polarion.d.ts +4 -0
- package/dist/export/adapters/polarion.d.ts.map +1 -0
- package/dist/export/adapters/polarion.js +67 -0
- package/dist/export/adapters/qtest.d.ts +4 -0
- package/dist/export/adapters/qtest.d.ts.map +1 -0
- package/dist/export/adapters/qtest.js +78 -0
- package/dist/export/adapters/qualityCenter.d.ts +3 -0
- package/dist/export/adapters/qualityCenter.d.ts.map +1 -0
- package/dist/export/adapters/qualityCenter.js +56 -0
- package/dist/export/adapters/spreadsheetSafeCsv.d.ts +36 -0
- package/dist/export/adapters/spreadsheetSafeCsv.d.ts.map +1 -0
- package/dist/export/adapters/spreadsheetSafeCsv.js +157 -0
- package/dist/export/adapters/traceability.d.ts +34 -0
- package/dist/export/adapters/traceability.d.ts.map +1 -0
- package/dist/export/adapters/traceability.js +142 -0
- package/dist/export/adapters/xray.d.ts +4 -0
- package/dist/export/adapters/xray.d.ts.map +1 -0
- package/dist/export/adapters/xray.js +72 -0
- package/dist/export/formats.d.ts +29 -0
- package/dist/export/formats.d.ts.map +1 -0
- package/dist/export/formats.js +34 -0
- package/dist/export/index.d.ts +4 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +10 -0
- package/dist/export/serialize.d.ts +17 -0
- package/dist/export/serialize.d.ts.map +1 -0
- package/dist/export/serialize.js +56 -0
- package/dist/export/textSafety.d.ts +15 -0
- package/dist/export/textSafety.d.ts.map +1 -0
- package/dist/export/textSafety.js +30 -0
- package/dist/generation/candidateBounds.d.ts +10 -0
- package/dist/generation/candidateBounds.d.ts.map +1 -0
- package/dist/generation/candidateBounds.js +14 -0
- package/dist/generation/index.d.ts +4 -0
- package/dist/generation/index.d.ts.map +1 -0
- package/dist/generation/index.js +20 -0
- package/dist/generation/parseGeneratedCandidates.d.ts +27 -0
- package/dist/generation/parseGeneratedCandidates.d.ts.map +1 -0
- package/dist/generation/parseGeneratedCandidates.js +253 -0
- package/dist/generation/prompt.d.ts +16 -0
- package/dist/generation/prompt.d.ts.map +1 -0
- package/dist/generation/prompt.js +151 -0
- package/dist/generation/requirementsIngestion.d.ts +21 -0
- package/dist/generation/requirementsIngestion.d.ts.map +1 -0
- package/dist/generation/requirementsIngestion.js +70 -0
- package/dist/hardening/index.d.ts +6 -0
- package/dist/hardening/index.d.ts.map +1 -0
- package/dist/hardening/index.js +8 -0
- package/dist/hardening/oversizeGuards.d.ts +21 -0
- package/dist/hardening/oversizeGuards.d.ts.map +1 -0
- package/dist/hardening/oversizeGuards.js +35 -0
- package/dist/hardening/pathSafety.d.ts +19 -0
- package/dist/hardening/pathSafety.d.ts.map +1 -0
- package/dist/hardening/pathSafety.js +61 -0
- package/dist/hardening/promptInjectionScrub.d.ts +17 -0
- package/dist/hardening/promptInjectionScrub.d.ts.map +1 -0
- package/dist/hardening/promptInjectionScrub.js +72 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/ingestion/adfParser.d.ts +61 -0
- package/dist/ingestion/adfParser.d.ts.map +1 -0
- package/dist/ingestion/adfParser.js +262 -0
- package/dist/ingestion/index.d.ts +6 -0
- package/dist/ingestion/index.d.ts.map +1 -0
- package/dist/ingestion/index.js +10 -0
- package/dist/ingestion/sourceMixPlanning.d.ts +36 -0
- package/dist/ingestion/sourceMixPlanning.d.ts.map +1 -0
- package/dist/ingestion/sourceMixPlanning.js +65 -0
- package/dist/ingestion/sourceReconciliation.d.ts +39 -0
- package/dist/ingestion/sourceReconciliation.d.ts.map +1 -0
- package/dist/ingestion/sourceReconciliation.js +74 -0
- package/dist/ingestion/untrustedContentNormalisation.d.ts +23 -0
- package/dist/ingestion/untrustedContentNormalisation.d.ts.map +1 -0
- package/dist/ingestion/untrustedContentNormalisation.js +121 -0
- package/dist/ingestion/workspaceAdapter.d.ts +55 -0
- package/dist/ingestion/workspaceAdapter.d.ts.map +1 -0
- package/dist/ingestion/workspaceAdapter.js +113 -0
- package/dist/review/auditEvents.d.ts +61 -0
- package/dist/review/auditEvents.d.ts.map +1 -0
- package/dist/review/auditEvents.js +50 -0
- package/dist/review/fourEyes.d.ts +24 -0
- package/dist/review/fourEyes.d.ts.map +1 -0
- package/dist/review/fourEyes.js +45 -0
- package/dist/review/index.d.ts +5 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/index.js +14 -0
- package/dist/review/lifecyclePolicy.d.ts +21 -0
- package/dist/review/lifecyclePolicy.d.ts.map +1 -0
- package/dist/review/lifecyclePolicy.js +38 -0
- package/dist/review/stateMachine.d.ts +28 -0
- package/dist/review/stateMachine.d.ts.map +1 -0
- package/dist/review/stateMachine.js +71 -0
- package/package.json +31 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Quality Intelligence test-quality rubric domain helpers (Epic #736, Issue #746).
|
|
2
|
+
//
|
|
3
|
+
// Pure functions — no IO, no model calls. Compute aggregate scores and verdict
|
|
4
|
+
// classifications from per-dimension rubric scores.
|
|
5
|
+
/** Threshold below which an overall score is classified as "weak". */
|
|
6
|
+
export const TEST_QUALITY_WEAK_THRESHOLD = 60;
|
|
7
|
+
/**
|
|
8
|
+
* Compute the mean score across all dimensions. Returns 0 when `dimensions` is empty.
|
|
9
|
+
* Deterministic: iteration order of `dimensions` does not affect the result.
|
|
10
|
+
*/
|
|
11
|
+
export function scoreFromDimensions(dimensions) {
|
|
12
|
+
if (dimensions.length === 0)
|
|
13
|
+
return 0;
|
|
14
|
+
let total = 0;
|
|
15
|
+
for (const d of dimensions) {
|
|
16
|
+
total += d.score;
|
|
17
|
+
}
|
|
18
|
+
return total / dimensions.length;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Classify an overall score as "weak" or "strong".
|
|
22
|
+
* Scores strictly below `TEST_QUALITY_WEAK_THRESHOLD` (60) are "weak"; 60 and above are "strong".
|
|
23
|
+
*/
|
|
24
|
+
export function verdictFromScore(score) {
|
|
25
|
+
return score < TEST_QUALITY_WEAK_THRESHOLD ? "weak" : "strong";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify a full rubric verdict. A candidate is only strong when the aggregate score and every
|
|
29
|
+
* mandatory rubric dimension meet the threshold; one weak dimension is enough to keep the finding
|
|
30
|
+
* visible instead of hiding it behind a passing average.
|
|
31
|
+
*/
|
|
32
|
+
export function verdictFromDimensions(dimensions) {
|
|
33
|
+
if (verdictFromScore(scoreFromDimensions(dimensions)) === "weak")
|
|
34
|
+
return "weak";
|
|
35
|
+
return dimensions.every((dimension) => dimension.score >= TEST_QUALITY_WEAK_THRESHOLD)
|
|
36
|
+
? "strong"
|
|
37
|
+
: "weak";
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Validate a list of candidates and return every emitted finding. Pure;
|
|
4
|
+
* deterministic; no IO. Empty `candidates` returns the empty array.
|
|
5
|
+
*/
|
|
6
|
+
export declare const validateCandidates: (runId: QualityIntelligence.QualityIntelligenceRunId, candidates: readonly QualityIntelligence.QualityIntelligenceTestCaseCandidate[]) => readonly QualityIntelligence.QualityIntelligenceValidationFinding[];
|
|
7
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/domain/validation.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AA+JpE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,mBAAmB,CAAC,wBAAwB,EACnD,YAAY,SAAS,mBAAmB,CAAC,oCAAoC,EAAE,KAC9E,SAAS,mBAAmB,CAAC,oCAAoC,EAoBnE,CAAC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Quality Intelligence validation (Epic #270, Issue #272).
|
|
2
|
+
//
|
|
3
|
+
// Pure schema/logic validators that emit `QualityIntelligenceValidationFinding`
|
|
4
|
+
// records. NO judge calls in this module — the model-assisted adversarial
|
|
5
|
+
// test-quality judge ships separately (Epic #736, keiko-server judgePort + the
|
|
6
|
+
// workflow judge stage) and augments these deterministic checks.
|
|
7
|
+
//
|
|
8
|
+
// v1 covers four deterministic checks per candidate:
|
|
9
|
+
// 1. schema-completeness — title/steps/expectedResults must be non-empty.
|
|
10
|
+
// 2. consecutive-step-repeat — an adjacent step must not canonically repeat the one before it
|
|
11
|
+
// (a context-changing step between two identical actions is allowed by design).
|
|
12
|
+
// 3. expected-presence — at least one expected result must be present.
|
|
13
|
+
// 4. trivial-contradiction — a precondition and an expected result share the
|
|
14
|
+
// same negation-stripped core but have opposite negation parity (XOR):
|
|
15
|
+
// exactly one of the two contains a negation word. Both positive or both
|
|
16
|
+
// negated → consistent; one positive + one negated → contradiction.
|
|
17
|
+
//
|
|
18
|
+
// Structurally inspired by
|
|
19
|
+
// Test Intelligence reference (TI) packages/core-engine/src/
|
|
20
|
+
// cross-field-invariant-engine.ts and acceptance-criteria.ts — but with
|
|
21
|
+
// the model-judge tier excluded and a Keiko-shaped finding output.
|
|
22
|
+
import { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
|
|
23
|
+
import { sha256Hex } from "@oscharko-dev/keiko-security";
|
|
24
|
+
import { normaliseCandidateText, normaliseText } from "./assertions.js";
|
|
25
|
+
const NEGATION_PATTERN = /\b(not|never|no longer|cannot|isn't|aren't|won't|doesn't|do not)\b/iu;
|
|
26
|
+
const collapseWhitespace = (value) => value.replace(/\s+/gu, " ").trim();
|
|
27
|
+
// Use normaliseCandidateText (NFKC + bidi/zero-width strip + trim) so that two candidates
|
|
28
|
+
// differing only by injected bidi or zero-width spoofing chars produce the same equivalence key.
|
|
29
|
+
const canonicaliseLine = (value) => collapseWhitespace(normaliseCandidateText(value).toLowerCase());
|
|
30
|
+
const stripNegation = (value) => value.replace(NEGATION_PATTERN, " ").replace(/\s+/gu, " ").trim();
|
|
31
|
+
const deriveFindingIdString = (runId, candidateId, kind, ordinal) => {
|
|
32
|
+
const payload = ["v1", String(runId), candidateId, kind, String(ordinal)].join("");
|
|
33
|
+
return `qi-finding-${sha256Hex(payload).slice(0, 32)}`;
|
|
34
|
+
};
|
|
35
|
+
const buildLogicDefect = (runId, candidate, ordinal, severity, summary) => {
|
|
36
|
+
const idString = deriveFindingIdString(runId, String(candidate.id), "logic-defect", ordinal);
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
kind: "logic-defect",
|
|
39
|
+
id: QualityIntelligence.asQualityIntelligenceValidationFindingId(idString),
|
|
40
|
+
runId,
|
|
41
|
+
candidateId: candidate.id,
|
|
42
|
+
severity,
|
|
43
|
+
summary,
|
|
44
|
+
evidenceAtomIds: Object.freeze([...candidate.derivedFromAtomIds]),
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const buildSemanticDefect = (runId, candidate, ordinal, severity, summary) => {
|
|
48
|
+
const idString = deriveFindingIdString(runId, String(candidate.id), "semantic-defect", ordinal);
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
kind: "semantic-defect",
|
|
51
|
+
id: QualityIntelligence.asQualityIntelligenceValidationFindingId(idString),
|
|
52
|
+
runId,
|
|
53
|
+
candidateId: candidate.id,
|
|
54
|
+
severity,
|
|
55
|
+
summary,
|
|
56
|
+
evidenceAtomIds: Object.freeze([...candidate.derivedFromAtomIds]),
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const checkSchemaCompleteness = (runId, candidate) => {
|
|
60
|
+
const findings = [];
|
|
61
|
+
if (normaliseText(candidate.title).length === 0) {
|
|
62
|
+
findings.push(buildLogicDefect(runId, candidate, 0, "high", "Candidate title is empty after NFKC trim."));
|
|
63
|
+
}
|
|
64
|
+
if (candidate.steps.length === 0) {
|
|
65
|
+
findings.push(buildLogicDefect(runId, candidate, 1, "high", "Candidate has no executable steps."));
|
|
66
|
+
}
|
|
67
|
+
return findings;
|
|
68
|
+
};
|
|
69
|
+
const checkExpectedResultsPresence = (runId, candidate) => {
|
|
70
|
+
if (candidate.expectedResults.length === 0) {
|
|
71
|
+
return [
|
|
72
|
+
buildLogicDefect(runId, candidate, 2, "high", "Candidate has no expected results recorded."),
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
};
|
|
77
|
+
const checkConsecutiveStepRepeat = (runId, candidate) => {
|
|
78
|
+
let previousCanonical = "";
|
|
79
|
+
for (const step of candidate.steps) {
|
|
80
|
+
const canonical = canonicaliseLine(step);
|
|
81
|
+
if (canonical.length === 0) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (canonical === previousCanonical) {
|
|
85
|
+
return [
|
|
86
|
+
buildLogicDefect(runId, candidate, 3, "medium", "Candidate step sequence contains a consecutive canonical-line repeat."),
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
previousCanonical = canonical;
|
|
90
|
+
}
|
|
91
|
+
return [];
|
|
92
|
+
};
|
|
93
|
+
const checkTrivialContradictions = (runId, candidate) => {
|
|
94
|
+
if (candidate.preconditions.length === 0 || candidate.expectedResults.length === 0) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
for (const result of candidate.expectedResults) {
|
|
98
|
+
const resultCanonical = canonicaliseLine(result);
|
|
99
|
+
const resultCore = stripNegation(resultCanonical);
|
|
100
|
+
if (resultCore.length === 0) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const negatedResult = NEGATION_PATTERN.test(resultCanonical);
|
|
104
|
+
for (const pre of candidate.preconditions) {
|
|
105
|
+
const preCanonical = canonicaliseLine(pre);
|
|
106
|
+
const preCore = stripNegation(preCanonical);
|
|
107
|
+
if (preCore.length === 0) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const negatedPre = NEGATION_PATTERN.test(preCanonical);
|
|
111
|
+
// Contradiction iff cores match AND exactly one side carries a negation (XOR parity).
|
|
112
|
+
if (preCore === resultCore && negatedPre !== negatedResult) {
|
|
113
|
+
return [
|
|
114
|
+
buildSemanticDefect(runId, candidate, 4, "medium", "Expected result trivially contradicts a precondition (post-negation match)."),
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Validate a list of candidates and return every emitted finding. Pure;
|
|
123
|
+
* deterministic; no IO. Empty `candidates` returns the empty array.
|
|
124
|
+
*/
|
|
125
|
+
export const validateCandidates = (runId, candidates) => {
|
|
126
|
+
if (candidates.length === 0) {
|
|
127
|
+
return Object.freeze([]);
|
|
128
|
+
}
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const candidate of candidates) {
|
|
131
|
+
for (const finding of checkSchemaCompleteness(runId, candidate)) {
|
|
132
|
+
out.push(finding);
|
|
133
|
+
}
|
|
134
|
+
for (const finding of checkExpectedResultsPresence(runId, candidate)) {
|
|
135
|
+
out.push(finding);
|
|
136
|
+
}
|
|
137
|
+
for (const finding of checkConsecutiveStepRepeat(runId, candidate)) {
|
|
138
|
+
out.push(finding);
|
|
139
|
+
}
|
|
140
|
+
for (const finding of checkTrivialContradictions(runId, candidate)) {
|
|
141
|
+
out.push(finding);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return Object.freeze(out);
|
|
145
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { QualityIntelligenceExportBundle, QualityIntelligenceTestCaseCandidate } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
export declare const ALM_CSV_HEADERS: readonly string[];
|
|
3
|
+
export declare function adaptToAlm(bundle: QualityIntelligenceExportBundle, candidates: readonly QualityIntelligenceTestCaseCandidate[]): string;
|
|
4
|
+
//# sourceMappingURL=alm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alm.d.ts","sourceRoot":"","sources":["../../../src/export/adapters/alm.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,+BAA+B,EAC/B,oCAAoC,EACrC,MAAM,+BAA+B,CAAC;AAIvC,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAS3C,CAAC;AA2CH,wBAAgB,UAAU,CACxB,MAAM,EAAE,+BAA+B,EACvC,UAAU,EAAE,SAAS,oCAAoC,EAAE,GAC1D,MAAM,CAkBR"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Micro Focus ALM (Quality Center) CSV export adapter (Epic #270, Issue #283).
|
|
2
|
+
//
|
|
3
|
+
// ALM's Excel-add-in bulk-import shape: Test Name, Description, Type, Designer,
|
|
4
|
+
// Subject, Step Name, Description (step), Expected Result. One row per (test, step)
|
|
5
|
+
// like qTest / Xray.
|
|
6
|
+
//
|
|
7
|
+
// TMS-bound: invariant asserted first. Pure-domain — NO HTTP, NO ALM SDK.
|
|
8
|
+
import { assertExportBundleInvariant } from "@oscharko-dev/keiko-contracts";
|
|
9
|
+
import { encodeSpreadsheetSafeRow } from "./spreadsheetSafeCsv.js";
|
|
10
|
+
export const ALM_CSV_HEADERS = Object.freeze([
|
|
11
|
+
"TestName",
|
|
12
|
+
"Description",
|
|
13
|
+
"Type",
|
|
14
|
+
"Designer",
|
|
15
|
+
"Subject",
|
|
16
|
+
"StepName",
|
|
17
|
+
"StepDescription",
|
|
18
|
+
"ExpectedResult",
|
|
19
|
+
]);
|
|
20
|
+
const ALM_DESIGNER = "keiko-quality-intelligence";
|
|
21
|
+
const buildDescription = (candidate) => {
|
|
22
|
+
if (candidate.preconditions.length === 0) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return `Preconditions: ${candidate.preconditions.join(" ; ")}`;
|
|
26
|
+
};
|
|
27
|
+
const buildSubject = (candidate) => {
|
|
28
|
+
return `Subject/${candidate.riskClass}`;
|
|
29
|
+
};
|
|
30
|
+
// Build the ALM rows for a single candidate. One row per (step, expected) pair — the row count is
|
|
31
|
+
// the longer of `steps`/`expectedResults` so a trailing expected result is never dropped (Issue
|
|
32
|
+
// #283); a candidate with neither yields one empty-step row.
|
|
33
|
+
function almRowsFor(candidate) {
|
|
34
|
+
const head = [
|
|
35
|
+
candidate.title,
|
|
36
|
+
buildDescription(candidate),
|
|
37
|
+
"MANUAL",
|
|
38
|
+
ALM_DESIGNER,
|
|
39
|
+
buildSubject(candidate),
|
|
40
|
+
];
|
|
41
|
+
const rowCount = Math.max(candidate.steps.length, candidate.expectedResults.length);
|
|
42
|
+
if (rowCount === 0) {
|
|
43
|
+
return encodeSpreadsheetSafeRow([...head, "", "", ""]);
|
|
44
|
+
}
|
|
45
|
+
let rows = "";
|
|
46
|
+
for (let i = 0; i < rowCount; i += 1) {
|
|
47
|
+
const stepName = i < candidate.steps.length ? `Step ${String(i + 1)}` : "";
|
|
48
|
+
rows += encodeSpreadsheetSafeRow([
|
|
49
|
+
...head,
|
|
50
|
+
stepName,
|
|
51
|
+
candidate.steps[i] ?? "",
|
|
52
|
+
candidate.expectedResults[i] ?? "",
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
return rows;
|
|
56
|
+
}
|
|
57
|
+
export function adaptToAlm(bundle, candidates) {
|
|
58
|
+
assertExportBundleInvariant(bundle);
|
|
59
|
+
const byId = new Map();
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
byId.set(candidate.id, candidate);
|
|
62
|
+
}
|
|
63
|
+
const sortedIds = bundle.contents
|
|
64
|
+
.map((entry) => entry.candidateId)
|
|
65
|
+
.slice()
|
|
66
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
67
|
+
let body = encodeSpreadsheetSafeRow(ALM_CSV_HEADERS);
|
|
68
|
+
for (const id of sortedIds) {
|
|
69
|
+
const candidate = byId.get(id);
|
|
70
|
+
if (candidate !== undefined) {
|
|
71
|
+
body += almRowsFor(candidate);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return body;
|
|
75
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { QualityIntelligenceExportBundle, QualityIntelligenceTestCaseCandidate } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
/** Schema headers for the Keiko-native CSV format. */
|
|
3
|
+
export declare const CSV_HEADERS: readonly string[];
|
|
4
|
+
export declare function adaptToCsv(bundle: QualityIntelligenceExportBundle, candidates: readonly QualityIntelligenceTestCaseCandidate[]): string;
|
|
5
|
+
//# sourceMappingURL=csv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../../../src/export/adapters/csv.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,+BAA+B,EAC/B,oCAAoC,EACrC,MAAM,+BAA+B,CAAC;AAIvC,sDAAsD;AACtD,eAAO,MAAM,WAAW,EAAE,SAAS,MAAM,EAcvC,CAAC;AAIH,wBAAgB,UAAU,CACxB,MAAM,EAAE,+BAA+B,EACvC,UAAU,EAAE,SAAS,oCAAoC,EAAE,GAC1D,MAAM,CAgCR"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Generic Keiko-native CSV export adapter (Epic #270, Issue #283).
|
|
2
|
+
//
|
|
3
|
+
// Pure-domain leaf. Reuses the spreadsheet-safe cell encoder so EVERY CSV
|
|
4
|
+
// adapter (Keiko-native, Jira, qTest, Xray, Polarion, ALM) shares the same
|
|
5
|
+
// formula-injection mitigation — the encoder is the single primitive.
|
|
6
|
+
import { assertExportBundleInvariant } from "@oscharko-dev/keiko-contracts";
|
|
7
|
+
import { encodeSpreadsheetSafeRow } from "./spreadsheetSafeCsv.js";
|
|
8
|
+
/** Schema headers for the Keiko-native CSV format. */
|
|
9
|
+
export const CSV_HEADERS = Object.freeze([
|
|
10
|
+
"CandidateId",
|
|
11
|
+
"RunId",
|
|
12
|
+
"Title",
|
|
13
|
+
"Priority",
|
|
14
|
+
"RiskClass",
|
|
15
|
+
"Status",
|
|
16
|
+
"Tags",
|
|
17
|
+
"Preconditions",
|
|
18
|
+
"Steps",
|
|
19
|
+
"ExpectedResults",
|
|
20
|
+
"DerivedFromAtomIds",
|
|
21
|
+
"CoverageMapRefs",
|
|
22
|
+
"FindingRefs",
|
|
23
|
+
]);
|
|
24
|
+
const joinSemicolon = (values) => values.join(" ; ");
|
|
25
|
+
export function adaptToCsv(bundle, candidates) {
|
|
26
|
+
assertExportBundleInvariant(bundle);
|
|
27
|
+
const byId = new Map();
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
byId.set(candidate.id, candidate);
|
|
30
|
+
}
|
|
31
|
+
const sortedEntries = [...bundle.contents].sort((a, b) => a.candidateId < b.candidateId ? -1 : a.candidateId > b.candidateId ? 1 : 0);
|
|
32
|
+
let body = encodeSpreadsheetSafeRow(CSV_HEADERS);
|
|
33
|
+
for (const entry of sortedEntries) {
|
|
34
|
+
const candidate = byId.get(entry.candidateId);
|
|
35
|
+
if (candidate === undefined) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
body += encodeSpreadsheetSafeRow([
|
|
39
|
+
candidate.id,
|
|
40
|
+
candidate.runId,
|
|
41
|
+
candidate.title,
|
|
42
|
+
candidate.priority,
|
|
43
|
+
candidate.riskClass,
|
|
44
|
+
candidate.status,
|
|
45
|
+
joinSemicolon(candidate.tags),
|
|
46
|
+
joinSemicolon(candidate.preconditions),
|
|
47
|
+
joinSemicolon(candidate.steps),
|
|
48
|
+
joinSemicolon(candidate.expectedResults),
|
|
49
|
+
joinSemicolon(candidate.derivedFromAtomIds),
|
|
50
|
+
joinSemicolon(entry.coverageMapRefs),
|
|
51
|
+
joinSemicolon(entry.findingRefs),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
return body;
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { adaptToAlm, ALM_CSV_HEADERS } from "./alm.js";
|
|
2
|
+
export { adaptToCsv, CSV_HEADERS } from "./csv.js";
|
|
3
|
+
export { adaptToJiraIssues, JIRA_CSV_HEADERS } from "./jira.js";
|
|
4
|
+
export { adaptToJson } from "./json.js";
|
|
5
|
+
export { adaptToPolarion, POLARION_CSV_HEADERS } from "./polarion.js";
|
|
6
|
+
export { adaptToQtest, QTEST_CSV_HEADERS } from "./qtest.js";
|
|
7
|
+
export { adaptToSpreadsheetSafeCsv, encodeSpreadsheetSafeCell, encodeSpreadsheetSafeRow, SPREADSHEET_FORMULA_LEAD_CHARS, SPREADSHEET_SAFE_CSV_HEADERS, startsWithFormulaLead, } from "./spreadsheetSafeCsv.js";
|
|
8
|
+
export { adaptToXray, XRAY_CSV_HEADERS } from "./xray.js";
|
|
9
|
+
export { adaptToMarkdown } from "./markdown.js";
|
|
10
|
+
export { adaptToPlainText } from "./plaintext.js";
|
|
11
|
+
export { adaptToQualityCenter } from "./qualityCenter.js";
|
|
12
|
+
export { adaptToTraceabilityCsv, adaptToTraceabilityMarkdown, TRACEABILITY_HEADERS, TRACEABILITY_REVERSE_HEADERS, type QualityIntelligenceTraceabilityDisplayOptions, type QualityIntelligenceTraceabilityRow, } from "./traceability.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/export/adapters/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,8BAA8B,EAC9B,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EACL,sBAAsB,EACtB,2BAA2B,EAC3B,oBAAoB,EACpB,4BAA4B,EAC5B,KAAK,6CAA6C,EAClD,KAAK,kCAAkC,GACxC,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Public barrel for the Quality Intelligence export adapters (Epic #270, Issue #283).
|
|
2
|
+
// Pure-domain leaf. Re-exports each adapter alongside its column-header constant so
|
|
3
|
+
// downstream consumers and tests have a single import surface.
|
|
4
|
+
export { adaptToAlm, ALM_CSV_HEADERS } from "./alm.js";
|
|
5
|
+
export { adaptToCsv, CSV_HEADERS } from "./csv.js";
|
|
6
|
+
export { adaptToJiraIssues, JIRA_CSV_HEADERS } from "./jira.js";
|
|
7
|
+
export { adaptToJson } from "./json.js";
|
|
8
|
+
export { adaptToPolarion, POLARION_CSV_HEADERS } from "./polarion.js";
|
|
9
|
+
export { adaptToQtest, QTEST_CSV_HEADERS } from "./qtest.js";
|
|
10
|
+
export { adaptToSpreadsheetSafeCsv, encodeSpreadsheetSafeCell, encodeSpreadsheetSafeRow, SPREADSHEET_FORMULA_LEAD_CHARS, SPREADSHEET_SAFE_CSV_HEADERS, startsWithFormulaLead, } from "./spreadsheetSafeCsv.js";
|
|
11
|
+
export { adaptToXray, XRAY_CSV_HEADERS } from "./xray.js";
|
|
12
|
+
export { adaptToMarkdown } from "./markdown.js";
|
|
13
|
+
export { adaptToPlainText } from "./plaintext.js";
|
|
14
|
+
export { adaptToQualityCenter } from "./qualityCenter.js";
|
|
15
|
+
export { adaptToTraceabilityCsv, adaptToTraceabilityMarkdown, TRACEABILITY_HEADERS, TRACEABILITY_REVERSE_HEADERS, } from "./traceability.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { QualityIntelligenceExportBundle, QualityIntelligenceTestCaseCandidate } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
/** Jira CSV columns; matches Jira's default "Bulk import" template. */
|
|
3
|
+
export declare const JIRA_CSV_HEADERS: readonly string[];
|
|
4
|
+
export declare function adaptToJiraIssues(bundle: QualityIntelligenceExportBundle, candidates: readonly QualityIntelligenceTestCaseCandidate[]): string;
|
|
5
|
+
//# sourceMappingURL=jira.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jira.d.ts","sourceRoot":"","sources":["../../../src/export/adapters/jira.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,+BAA+B,EAC/B,oCAAoC,EACrC,MAAM,+BAA+B,CAAC;AAIvC,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAM5C,CAAC;AAmCH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,+BAA+B,EACvC,UAAU,EAAE,SAAS,oCAAoC,EAAE,GAC1D,MAAM,CA0BR"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Jira Issues CSV export adapter (Epic #270, Issue #283).
|
|
2
|
+
//
|
|
3
|
+
// Produces a Jira CSV in the canonical `Summary, Description, IssueType, Priority,
|
|
4
|
+
// Labels` shape. The output is the byte body only — there is NO HTTP, NO Jira REST
|
|
5
|
+
// SDK import, and NO authentication concern in this leaf. Live publishing of these
|
|
6
|
+
// CSVs to a Jira tenant is deferred to a future issue alongside connector
|
|
7
|
+
// authorisation (#278).
|
|
8
|
+
//
|
|
9
|
+
// TMS-bound adapter: `assertExportBundleInvariant` is invoked first, which enforces
|
|
10
|
+
// `redactionAttested === true` on the bundle. A non-attested bundle throws before
|
|
11
|
+
// any candidate is read.
|
|
12
|
+
//
|
|
13
|
+
// Pure-domain leaf. Reuses the shared spreadsheet-safe cell encoder so all CSV
|
|
14
|
+
// adapters share one formula-injection mitigation primitive.
|
|
15
|
+
import { assertExportBundleInvariant } from "@oscharko-dev/keiko-contracts";
|
|
16
|
+
import { encodeSpreadsheetSafeRow } from "./spreadsheetSafeCsv.js";
|
|
17
|
+
/** Jira CSV columns; matches Jira's default "Bulk import" template. */
|
|
18
|
+
export const JIRA_CSV_HEADERS = Object.freeze([
|
|
19
|
+
"Summary",
|
|
20
|
+
"Description",
|
|
21
|
+
"IssueType",
|
|
22
|
+
"Priority",
|
|
23
|
+
"Labels",
|
|
24
|
+
]);
|
|
25
|
+
/** Jira `Issue Type` value used for QI exports. Spec: test cases land as Tests. */
|
|
26
|
+
const JIRA_ISSUE_TYPE = "Test";
|
|
27
|
+
const buildDescription = (candidate) => {
|
|
28
|
+
const sections = [];
|
|
29
|
+
if (candidate.preconditions.length > 0) {
|
|
30
|
+
sections.push(`Preconditions:\n${candidate.preconditions.map((p) => `- ${p}`).join("\n")}`);
|
|
31
|
+
}
|
|
32
|
+
if (candidate.steps.length > 0) {
|
|
33
|
+
sections.push(`Steps:\n${candidate.steps.map((step, i) => `${String(i + 1)}. ${step}`).join("\n")}`);
|
|
34
|
+
}
|
|
35
|
+
if (candidate.expectedResults.length > 0) {
|
|
36
|
+
sections.push(`Expected:\n${candidate.expectedResults.map((e) => `- ${e}`).join("\n")}`);
|
|
37
|
+
}
|
|
38
|
+
return sections.join("\n\n");
|
|
39
|
+
};
|
|
40
|
+
const mapPriority = (priority) => {
|
|
41
|
+
// Jira default priorities: Highest / High / Medium / Low / Lowest.
|
|
42
|
+
switch (priority) {
|
|
43
|
+
case "P0":
|
|
44
|
+
return "Highest";
|
|
45
|
+
case "P1":
|
|
46
|
+
return "High";
|
|
47
|
+
case "P2":
|
|
48
|
+
return "Medium";
|
|
49
|
+
case "P3":
|
|
50
|
+
return "Low";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export function adaptToJiraIssues(bundle, candidates) {
|
|
54
|
+
assertExportBundleInvariant(bundle);
|
|
55
|
+
const byId = new Map();
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
byId.set(candidate.id, candidate);
|
|
58
|
+
}
|
|
59
|
+
const sortedIds = bundle.contents
|
|
60
|
+
.map((entry) => entry.candidateId)
|
|
61
|
+
.slice()
|
|
62
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
63
|
+
let body = encodeSpreadsheetSafeRow(JIRA_CSV_HEADERS);
|
|
64
|
+
for (const id of sortedIds) {
|
|
65
|
+
const candidate = byId.get(id);
|
|
66
|
+
if (candidate === undefined) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const labels = [candidate.riskClass, ...candidate.tags].join(" ");
|
|
70
|
+
body += encodeSpreadsheetSafeRow([
|
|
71
|
+
candidate.title,
|
|
72
|
+
buildDescription(candidate),
|
|
73
|
+
JIRA_ISSUE_TYPE,
|
|
74
|
+
mapPriority(candidate.priority),
|
|
75
|
+
labels,
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
return body;
|
|
79
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { QualityIntelligenceExportBundle, QualityIntelligenceTestCaseCandidate } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
export declare function adaptToJson(bundle: QualityIntelligenceExportBundle, candidates: readonly QualityIntelligenceTestCaseCandidate[]): string;
|
|
3
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../../src/export/adapters/json.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,+BAA+B,EAE/B,oCAAoC,EACrC,MAAM,+BAA+B,CAAC;AAmCvC,wBAAgB,WAAW,CACzB,MAAM,EAAE,+BAA+B,EACvC,UAAU,EAAE,SAAS,oCAAoC,EAAE,GAC1D,MAAM,CAwCR"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Typed JSON export adapter (Epic #270, Issue #283).
|
|
2
|
+
//
|
|
3
|
+
// Produces a deterministic JSON document with stable key ordering and stable
|
|
4
|
+
// candidate ordering. Object keys appear in the order written (V8 insertion
|
|
5
|
+
// order preservation); candidate / entry arrays are sorted by id to keep
|
|
6
|
+
// byte-for-byte determinism across runs.
|
|
7
|
+
//
|
|
8
|
+
// Pure-domain leaf. Reuses `assertExportBundleInvariant`. JSON is a portable
|
|
9
|
+
// target so the redactionAttested invariant fires only when the bundle's
|
|
10
|
+
// adapter is TMS-bound — pure `json` adapter does not require attestation by
|
|
11
|
+
// design (matches the contract-side TMS adapter set).
|
|
12
|
+
import { assertExportBundleInvariant } from "@oscharko-dev/keiko-contracts";
|
|
13
|
+
const byCandidateIdAsc = (a, b) => (a.candidateId < b.candidateId ? -1 : a.candidateId > b.candidateId ? 1 : 0);
|
|
14
|
+
export function adaptToJson(bundle, candidates) {
|
|
15
|
+
assertExportBundleInvariant(bundle);
|
|
16
|
+
const byId = new Map();
|
|
17
|
+
for (const candidate of candidates) {
|
|
18
|
+
byId.set(candidate.id, candidate);
|
|
19
|
+
}
|
|
20
|
+
const sortedEntries = [...bundle.contents].sort(byCandidateIdAsc);
|
|
21
|
+
const payloadCandidates = [];
|
|
22
|
+
for (const entry of sortedEntries) {
|
|
23
|
+
const candidate = byId.get(entry.candidateId);
|
|
24
|
+
if (candidate === undefined) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
payloadCandidates.push({
|
|
28
|
+
id: candidate.id,
|
|
29
|
+
runId: candidate.runId,
|
|
30
|
+
title: candidate.title,
|
|
31
|
+
priority: candidate.priority,
|
|
32
|
+
riskClass: candidate.riskClass,
|
|
33
|
+
status: candidate.status,
|
|
34
|
+
tags: candidate.tags,
|
|
35
|
+
preconditions: candidate.preconditions,
|
|
36
|
+
steps: candidate.steps,
|
|
37
|
+
expectedResults: candidate.expectedResults,
|
|
38
|
+
derivedFromAtomIds: candidate.derivedFromAtomIds,
|
|
39
|
+
coverageMapRefs: entry.coverageMapRefs,
|
|
40
|
+
findingRefs: entry.findingRefs,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const envelope = {
|
|
44
|
+
schemaVersion: "1",
|
|
45
|
+
bundleId: bundle.id,
|
|
46
|
+
runId: bundle.runId,
|
|
47
|
+
targetAdapter: bundle.targetAdapter,
|
|
48
|
+
createdAt: bundle.createdAt,
|
|
49
|
+
integrityHashSha256Hex: bundle.integrityHashSha256Hex,
|
|
50
|
+
redactionAttested: bundle.redactionAttested,
|
|
51
|
+
candidates: payloadCandidates,
|
|
52
|
+
};
|
|
53
|
+
return JSON.stringify(envelope, null, 2);
|
|
54
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { QualityIntelligenceExportBundle, QualityIntelligenceTestCaseCandidate } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
export declare function adaptToMarkdown(bundle: QualityIntelligenceExportBundle, candidates: readonly QualityIntelligenceTestCaseCandidate[]): string;
|
|
3
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../src/export/adapters/markdown.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,+BAA+B,EAC/B,oCAAoC,EACrC,MAAM,+BAA+B,CAAC;AAyEvC,wBAAgB,eAAe,CAC7B,MAAM,EAAE,+BAA+B,EACvC,UAAU,EAAE,SAAS,oCAAoC,EAAE,GAC1D,MAAM,CAqBR"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Markdown export adapter (Epic #711).
|
|
2
|
+
//
|
|
3
|
+
// Produces a deterministic Markdown document with one section per candidate,
|
|
4
|
+
// sorted by candidateId ascending. No timestamps, no random content. Pure
|
|
5
|
+
// string output — byte-identical for identical input.
|
|
6
|
+
//
|
|
7
|
+
// Pure-domain leaf. NO IO. NO node:* imports. NO new runtime dependency.
|
|
8
|
+
import { assertExportBundleInvariant } from "@oscharko-dev/keiko-contracts";
|
|
9
|
+
import { inlineField } from "../textSafety.js";
|
|
10
|
+
import { startsWithFormulaLead } from "./spreadsheetSafeCsv.js";
|
|
11
|
+
const byCandidateIdAsc = (a, b) => a.candidateId < b.candidateId ? -1 : a.candidateId > b.candidateId ? 1 : 0;
|
|
12
|
+
// Untrusted candidate free-text rendered into the EXPORTED Markdown artifact must not inject active
|
|
13
|
+
// Markdown structure into an external viewer, nor evaluate as a formula if the .md is pasted into a
|
|
14
|
+
// spreadsheet (Issue #284 AC2 — "Generated artifacts are sanitized before preview or export").
|
|
15
|
+
// `mdText` composes content-preserving (escape-not-strip) steps on top of inlineField:
|
|
16
|
+
// 1. inlineField — fold line breaks so the value stays one logical unit (Epic #711);
|
|
17
|
+
// 2. neutralise a leading spreadsheet formula lead (=,+,-,@, incl. a trimmed leading-whitespace
|
|
18
|
+
// run) with a single-quote prefix — parity with the CSV adapters / traceability.mdCell;
|
|
19
|
+
// 3. escape the Markdown-active vectors a single-line value can still smuggle — a link or image
|
|
20
|
+
// (incl. javascript:/data: hrefs) and a fenced-code run — the link/image/fenced subset of the
|
|
21
|
+
// accepted #278 untrusted-markdown escape set (ingestion/untrustedContentNormalisation). The
|
|
22
|
+
// literal text is preserved so an auditor still reads the original content.
|
|
23
|
+
// The #278 set's fourth vector — a line-leading `#` heading — is intentionally NOT escaped here: it
|
|
24
|
+
// is already mitigated structurally, not by escaping. Step 1 folds every internal line break to a
|
|
25
|
+
// space, and every field value is rendered behind a fixed prefix (`## ` for the title, `- `/`N. `
|
|
26
|
+
// for list items, `**Tags:** ` etc.), so a leading `#` in untrusted text can never reach a true
|
|
27
|
+
// line-start and open a heading.
|
|
28
|
+
const FENCED_CODE = /```/gu;
|
|
29
|
+
const IMAGE_OPEN = /!\[/gu;
|
|
30
|
+
const LINK_OPEN = /(?<!!)\[([^\]]*)\]\(/gu;
|
|
31
|
+
function mdText(value) {
|
|
32
|
+
const oneLine = inlineField(value);
|
|
33
|
+
const formulaSafe = startsWithFormulaLead(oneLine) ? `'${oneLine}` : oneLine;
|
|
34
|
+
return formulaSafe
|
|
35
|
+
.replace(FENCED_CODE, "\\`\\`\\`")
|
|
36
|
+
.replace(IMAGE_OPEN, "\\!\\[")
|
|
37
|
+
.replace(LINK_OPEN, (_match, inner) => `\\[${inner}\\](`);
|
|
38
|
+
}
|
|
39
|
+
const mdTextList = (items) => items.map(mdText);
|
|
40
|
+
const mdList = (items) => items.length === 0
|
|
41
|
+
? "_none_\n"
|
|
42
|
+
: mdTextList(items)
|
|
43
|
+
.map((item) => `- ${item}`)
|
|
44
|
+
.join("\n") + "\n";
|
|
45
|
+
function renderCandidate(candidate, runId) {
|
|
46
|
+
const lines = [];
|
|
47
|
+
lines.push(`## ${mdText(candidate.title)}\n`);
|
|
48
|
+
lines.push(`**ID:** ${candidate.id} `);
|
|
49
|
+
lines.push(`**Run:** ${runId} `);
|
|
50
|
+
lines.push(`**Priority:** ${candidate.priority} `);
|
|
51
|
+
lines.push(`**Risk class:** ${candidate.riskClass} `);
|
|
52
|
+
lines.push(`**Status:** ${candidate.status} `);
|
|
53
|
+
lines.push(`**Tags:** ${candidate.tags.length > 0 ? mdTextList(candidate.tags).join(", ") : "_none_"} `);
|
|
54
|
+
lines.push("");
|
|
55
|
+
lines.push("### Preconditions\n");
|
|
56
|
+
lines.push(mdList(candidate.preconditions));
|
|
57
|
+
lines.push("### Steps\n");
|
|
58
|
+
lines.push(candidate.steps.length === 0
|
|
59
|
+
? "_none_\n"
|
|
60
|
+
: mdTextList(candidate.steps)
|
|
61
|
+
.map((s, i) => `${String(i + 1)}. ${s}`)
|
|
62
|
+
.join("\n") + "\n");
|
|
63
|
+
lines.push("### Expected results\n");
|
|
64
|
+
lines.push(mdList(candidate.expectedResults));
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
export function adaptToMarkdown(bundle, candidates) {
|
|
68
|
+
assertExportBundleInvariant(bundle);
|
|
69
|
+
const byId = new Map();
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
byId.set(candidate.id, candidate);
|
|
72
|
+
}
|
|
73
|
+
const sortedEntries = [...bundle.contents].sort(byCandidateIdAsc);
|
|
74
|
+
const sections = [];
|
|
75
|
+
sections.push(`# Quality Intelligence Export\n`);
|
|
76
|
+
sections.push(`**Bundle:** ${bundle.id} `);
|
|
77
|
+
sections.push(`**Run:** ${bundle.runId} `);
|
|
78
|
+
sections.push(`**Adapter:** ${bundle.targetAdapter} `);
|
|
79
|
+
sections.push("");
|
|
80
|
+
for (const entry of sortedEntries) {
|
|
81
|
+
const candidate = byId.get(entry.candidateId);
|
|
82
|
+
if (candidate === undefined) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
sections.push(renderCandidate(candidate, bundle.runId));
|
|
86
|
+
}
|
|
87
|
+
return sections.join("\n") + "\n";
|
|
88
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { QualityIntelligenceExportBundle, QualityIntelligenceTestCaseCandidate } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
export declare function adaptToPlainText(bundle: QualityIntelligenceExportBundle, candidates: readonly QualityIntelligenceTestCaseCandidate[]): string;
|
|
3
|
+
//# sourceMappingURL=plaintext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plaintext.d.ts","sourceRoot":"","sources":["../../../src/export/adapters/plaintext.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,+BAA+B,EAC/B,oCAAoC,EACrC,MAAM,+BAA+B,CAAC;AA2CvC,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,+BAA+B,EACvC,UAAU,EAAE,SAAS,oCAAoC,EAAE,GAC1D,MAAM,CA0BR"}
|