@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.
Files changed (203) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/__tests__/_fixtureLoader.d.ts +9 -0
  3. package/dist/__tests__/_fixtureLoader.d.ts.map +1 -0
  4. package/dist/__tests__/_fixtureLoader.js +75 -0
  5. package/dist/domain/assertions.d.ts +61 -0
  6. package/dist/domain/assertions.d.ts.map +1 -0
  7. package/dist/domain/assertions.js +134 -0
  8. package/dist/domain/coverageRelevance.d.ts +73 -0
  9. package/dist/domain/coverageRelevance.d.ts.map +1 -0
  10. package/dist/domain/coverageRelevance.js +155 -0
  11. package/dist/domain/deduplication.d.ts +17 -0
  12. package/dist/domain/deduplication.d.ts.map +1 -0
  13. package/dist/domain/deduplication.js +95 -0
  14. package/dist/domain/figma/a11yBaseline.d.ts +17 -0
  15. package/dist/domain/figma/a11yBaseline.d.ts.map +1 -0
  16. package/dist/domain/figma/a11yBaseline.js +218 -0
  17. package/dist/domain/figma/cleanToScreenIr.d.ts +3 -0
  18. package/dist/domain/figma/cleanToScreenIr.d.ts.map +1 -0
  19. package/dist/domain/figma/cleanToScreenIr.js +62 -0
  20. package/dist/domain/figma/codeTargetAdapter.d.ts +30 -0
  21. package/dist/domain/figma/codeTargetAdapter.d.ts.map +1 -0
  22. package/dist/domain/figma/codeTargetAdapter.js +27 -0
  23. package/dist/domain/figma/color.d.ts +31 -0
  24. package/dist/domain/figma/color.d.ts.map +1 -0
  25. package/dist/domain/figma/color.js +99 -0
  26. package/dist/domain/figma/emissionPlan.d.ts +56 -0
  27. package/dist/domain/figma/emissionPlan.d.ts.map +1 -0
  28. package/dist/domain/figma/emissionPlan.js +87 -0
  29. package/dist/domain/figma/htmlCssAdapter.d.ts +13 -0
  30. package/dist/domain/figma/htmlCssAdapter.d.ts.map +1 -0
  31. package/dist/domain/figma/htmlCssAdapter.js +452 -0
  32. package/dist/domain/figma/index.d.ts +19 -0
  33. package/dist/domain/figma/index.d.ts.map +1 -0
  34. package/dist/domain/figma/index.js +31 -0
  35. package/dist/domain/figma/irTypes.d.ts +156 -0
  36. package/dist/domain/figma/irTypes.d.ts.map +1 -0
  37. package/dist/domain/figma/irTypes.js +8 -0
  38. package/dist/domain/figma/links.d.ts +6 -0
  39. package/dist/domain/figma/links.d.ts.map +1 -0
  40. package/dist/domain/figma/links.js +102 -0
  41. package/dist/domain/figma/navGraph.d.ts +74 -0
  42. package/dist/domain/figma/navGraph.d.ts.map +1 -0
  43. package/dist/domain/figma/navGraph.js +315 -0
  44. package/dist/domain/figma/normalize.d.ts +7 -0
  45. package/dist/domain/figma/normalize.d.ts.map +1 -0
  46. package/dist/domain/figma/normalize.js +252 -0
  47. package/dist/domain/figma/prune.d.ts +15 -0
  48. package/dist/domain/figma/prune.d.ts.map +1 -0
  49. package/dist/domain/figma/prune.js +65 -0
  50. package/dist/domain/figma/screenDetect.d.ts +8 -0
  51. package/dist/domain/figma/screenDetect.d.ts.map +1 -0
  52. package/dist/domain/figma/screenDetect.js +35 -0
  53. package/dist/domain/figma/screenIrTestBaseline.d.ts +52 -0
  54. package/dist/domain/figma/screenIrTestBaseline.d.ts.map +1 -0
  55. package/dist/domain/figma/screenIrTestBaseline.js +326 -0
  56. package/dist/domain/figma/semanticNaming.d.ts +24 -0
  57. package/dist/domain/figma/semanticNaming.d.ts.map +1 -0
  58. package/dist/domain/figma/semanticNaming.js +67 -0
  59. package/dist/domain/figma/sourceNode.d.ts +24 -0
  60. package/dist/domain/figma/sourceNode.d.ts.map +1 -0
  61. package/dist/domain/figma/sourceNode.js +26 -0
  62. package/dist/domain/figma/tokens.d.ts +11 -0
  63. package/dist/domain/figma/tokens.d.ts.map +1 -0
  64. package/dist/domain/figma/tokens.js +148 -0
  65. package/dist/domain/figma/visionAugmentation.d.ts +14 -0
  66. package/dist/domain/figma/visionAugmentation.d.ts.map +1 -0
  67. package/dist/domain/figma/visionAugmentation.js +48 -0
  68. package/dist/domain/intentDerivation.d.ts +21 -0
  69. package/dist/domain/intentDerivation.d.ts.map +1 -0
  70. package/dist/domain/intentDerivation.js +126 -0
  71. package/dist/domain/policyProfile.d.ts +37 -0
  72. package/dist/domain/policyProfile.d.ts.map +1 -0
  73. package/dist/domain/policyProfile.js +94 -0
  74. package/dist/domain/requirementExcerpt.d.ts +9 -0
  75. package/dist/domain/requirementExcerpt.d.ts.map +1 -0
  76. package/dist/domain/requirementExcerpt.js +39 -0
  77. package/dist/domain/staleness.d.ts +56 -0
  78. package/dist/domain/staleness.d.ts.map +1 -0
  79. package/dist/domain/staleness.js +313 -0
  80. package/dist/domain/testDesignModel.d.ts +38 -0
  81. package/dist/domain/testDesignModel.d.ts.map +1 -0
  82. package/dist/domain/testDesignModel.js +264 -0
  83. package/dist/domain/testQualityRubric.d.ts +20 -0
  84. package/dist/domain/testQualityRubric.d.ts.map +1 -0
  85. package/dist/domain/testQualityRubric.js +38 -0
  86. package/dist/domain/validation.d.ts +7 -0
  87. package/dist/domain/validation.d.ts.map +1 -0
  88. package/dist/domain/validation.js +145 -0
  89. package/dist/export/adapters/alm.d.ts +4 -0
  90. package/dist/export/adapters/alm.d.ts.map +1 -0
  91. package/dist/export/adapters/alm.js +75 -0
  92. package/dist/export/adapters/csv.d.ts +5 -0
  93. package/dist/export/adapters/csv.d.ts.map +1 -0
  94. package/dist/export/adapters/csv.js +55 -0
  95. package/dist/export/adapters/index.d.ts +13 -0
  96. package/dist/export/adapters/index.d.ts.map +1 -0
  97. package/dist/export/adapters/index.js +15 -0
  98. package/dist/export/adapters/jira.d.ts +5 -0
  99. package/dist/export/adapters/jira.d.ts.map +1 -0
  100. package/dist/export/adapters/jira.js +79 -0
  101. package/dist/export/adapters/json.d.ts +3 -0
  102. package/dist/export/adapters/json.d.ts.map +1 -0
  103. package/dist/export/adapters/json.js +54 -0
  104. package/dist/export/adapters/markdown.d.ts +3 -0
  105. package/dist/export/adapters/markdown.d.ts.map +1 -0
  106. package/dist/export/adapters/markdown.js +88 -0
  107. package/dist/export/adapters/plaintext.d.ts +3 -0
  108. package/dist/export/adapters/plaintext.d.ts.map +1 -0
  109. package/dist/export/adapters/plaintext.js +65 -0
  110. package/dist/export/adapters/polarion.d.ts +4 -0
  111. package/dist/export/adapters/polarion.d.ts.map +1 -0
  112. package/dist/export/adapters/polarion.js +67 -0
  113. package/dist/export/adapters/qtest.d.ts +4 -0
  114. package/dist/export/adapters/qtest.d.ts.map +1 -0
  115. package/dist/export/adapters/qtest.js +78 -0
  116. package/dist/export/adapters/qualityCenter.d.ts +3 -0
  117. package/dist/export/adapters/qualityCenter.d.ts.map +1 -0
  118. package/dist/export/adapters/qualityCenter.js +56 -0
  119. package/dist/export/adapters/spreadsheetSafeCsv.d.ts +36 -0
  120. package/dist/export/adapters/spreadsheetSafeCsv.d.ts.map +1 -0
  121. package/dist/export/adapters/spreadsheetSafeCsv.js +157 -0
  122. package/dist/export/adapters/traceability.d.ts +34 -0
  123. package/dist/export/adapters/traceability.d.ts.map +1 -0
  124. package/dist/export/adapters/traceability.js +142 -0
  125. package/dist/export/adapters/xray.d.ts +4 -0
  126. package/dist/export/adapters/xray.d.ts.map +1 -0
  127. package/dist/export/adapters/xray.js +72 -0
  128. package/dist/export/formats.d.ts +29 -0
  129. package/dist/export/formats.d.ts.map +1 -0
  130. package/dist/export/formats.js +34 -0
  131. package/dist/export/index.d.ts +4 -0
  132. package/dist/export/index.d.ts.map +1 -0
  133. package/dist/export/index.js +10 -0
  134. package/dist/export/serialize.d.ts +17 -0
  135. package/dist/export/serialize.d.ts.map +1 -0
  136. package/dist/export/serialize.js +56 -0
  137. package/dist/export/textSafety.d.ts +15 -0
  138. package/dist/export/textSafety.d.ts.map +1 -0
  139. package/dist/export/textSafety.js +30 -0
  140. package/dist/generation/candidateBounds.d.ts +10 -0
  141. package/dist/generation/candidateBounds.d.ts.map +1 -0
  142. package/dist/generation/candidateBounds.js +14 -0
  143. package/dist/generation/index.d.ts +4 -0
  144. package/dist/generation/index.d.ts.map +1 -0
  145. package/dist/generation/index.js +20 -0
  146. package/dist/generation/parseGeneratedCandidates.d.ts +27 -0
  147. package/dist/generation/parseGeneratedCandidates.d.ts.map +1 -0
  148. package/dist/generation/parseGeneratedCandidates.js +253 -0
  149. package/dist/generation/prompt.d.ts +16 -0
  150. package/dist/generation/prompt.d.ts.map +1 -0
  151. package/dist/generation/prompt.js +151 -0
  152. package/dist/generation/requirementsIngestion.d.ts +21 -0
  153. package/dist/generation/requirementsIngestion.d.ts.map +1 -0
  154. package/dist/generation/requirementsIngestion.js +70 -0
  155. package/dist/hardening/index.d.ts +6 -0
  156. package/dist/hardening/index.d.ts.map +1 -0
  157. package/dist/hardening/index.js +8 -0
  158. package/dist/hardening/oversizeGuards.d.ts +21 -0
  159. package/dist/hardening/oversizeGuards.d.ts.map +1 -0
  160. package/dist/hardening/oversizeGuards.js +35 -0
  161. package/dist/hardening/pathSafety.d.ts +19 -0
  162. package/dist/hardening/pathSafety.d.ts.map +1 -0
  163. package/dist/hardening/pathSafety.js +61 -0
  164. package/dist/hardening/promptInjectionScrub.d.ts +17 -0
  165. package/dist/hardening/promptInjectionScrub.d.ts.map +1 -0
  166. package/dist/hardening/promptInjectionScrub.js +72 -0
  167. package/dist/index.d.ts +22 -0
  168. package/dist/index.d.ts.map +1 -0
  169. package/dist/index.js +44 -0
  170. package/dist/ingestion/adfParser.d.ts +61 -0
  171. package/dist/ingestion/adfParser.d.ts.map +1 -0
  172. package/dist/ingestion/adfParser.js +262 -0
  173. package/dist/ingestion/index.d.ts +6 -0
  174. package/dist/ingestion/index.d.ts.map +1 -0
  175. package/dist/ingestion/index.js +10 -0
  176. package/dist/ingestion/sourceMixPlanning.d.ts +36 -0
  177. package/dist/ingestion/sourceMixPlanning.d.ts.map +1 -0
  178. package/dist/ingestion/sourceMixPlanning.js +65 -0
  179. package/dist/ingestion/sourceReconciliation.d.ts +39 -0
  180. package/dist/ingestion/sourceReconciliation.d.ts.map +1 -0
  181. package/dist/ingestion/sourceReconciliation.js +74 -0
  182. package/dist/ingestion/untrustedContentNormalisation.d.ts +23 -0
  183. package/dist/ingestion/untrustedContentNormalisation.d.ts.map +1 -0
  184. package/dist/ingestion/untrustedContentNormalisation.js +121 -0
  185. package/dist/ingestion/workspaceAdapter.d.ts +55 -0
  186. package/dist/ingestion/workspaceAdapter.d.ts.map +1 -0
  187. package/dist/ingestion/workspaceAdapter.js +113 -0
  188. package/dist/review/auditEvents.d.ts +61 -0
  189. package/dist/review/auditEvents.d.ts.map +1 -0
  190. package/dist/review/auditEvents.js +50 -0
  191. package/dist/review/fourEyes.d.ts +24 -0
  192. package/dist/review/fourEyes.d.ts.map +1 -0
  193. package/dist/review/fourEyes.js +45 -0
  194. package/dist/review/index.d.ts +5 -0
  195. package/dist/review/index.d.ts.map +1 -0
  196. package/dist/review/index.js +14 -0
  197. package/dist/review/lifecyclePolicy.d.ts +21 -0
  198. package/dist/review/lifecyclePolicy.d.ts.map +1 -0
  199. package/dist/review/lifecyclePolicy.js +38 -0
  200. package/dist/review/stateMachine.d.ts +28 -0
  201. package/dist/review/stateMachine.d.ts.map +1 -0
  202. package/dist/review/stateMachine.js +71 -0
  203. package/package.json +31 -0
@@ -0,0 +1,65 @@
1
+ // Quality Intelligence — source-mix planning (Epic #270, Issue #278).
2
+ //
3
+ // Deterministic planner that takes a list of `QualityIntelligenceSourceEnvelope`s and
4
+ // returns a `SourceMixPlan`: a dedup'd, priority-ordered list of envelope ids together
5
+ // with an oversize-flag table the consumer uses to decide what to skip.
6
+ //
7
+ // Pure: no IO, no clock, no randomness. Operates only on contract types from
8
+ // @oscharko-dev/keiko-contracts.
9
+ //
10
+ // Structurally inspired by Test Intelligence reference (TI) source-mix planning, but the
11
+ // envelope-ref shape and the priority table are anchored on the Keiko contracts surface.
12
+ /** Per-kind priority weight: lower number = higher priority (planned earlier). */
13
+ export const SOURCE_KIND_PRIORITY = {
14
+ "repository-context": 0,
15
+ "local-knowledge-capsule": 1,
16
+ "human-context": 2,
17
+ "figma-evidence": 3,
18
+ "connector-document": 4,
19
+ };
20
+ const DEFAULT_MAX_LABEL_BYTES = 256;
21
+ const utf8ByteLength = (value) => new TextEncoder().encode(value).length;
22
+ const stableSecondaryKey = (envelope) => `${envelope.kind}\u0000${envelope.id}`;
23
+ const compareEntries = (a, b) => {
24
+ if (a.priority !== b.priority)
25
+ return a.priority - b.priority;
26
+ const ka = `${a.kind}\u0000${a.envelopeId}`;
27
+ const kb = `${b.kind}\u0000${b.envelopeId}`;
28
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
29
+ };
30
+ /**
31
+ * Build a deterministic plan from a list of envelopes. Stable, pure.
32
+ */
33
+ export const planSourceMix = (envelopes, options = {}) => {
34
+ const maxLabelBytes = options.maxLabelBytes ?? DEFAULT_MAX_LABEL_BYTES;
35
+ const maxEnvelopes = options.maxEnvelopes ?? 256;
36
+ const seen = new Set();
37
+ const droppedForDuplicate = [];
38
+ const candidate = [];
39
+ for (const envelope of envelopes) {
40
+ const key = stableSecondaryKey(envelope);
41
+ if (seen.has(key)) {
42
+ droppedForDuplicate.push(envelope.id);
43
+ continue;
44
+ }
45
+ seen.add(key);
46
+ const oversize = utf8ByteLength(envelope.displayLabel) > maxLabelBytes;
47
+ candidate.push({
48
+ envelopeId: envelope.id,
49
+ kind: envelope.kind,
50
+ priority: SOURCE_KIND_PRIORITY[envelope.kind],
51
+ oversize,
52
+ });
53
+ }
54
+ const sorted = [...candidate].sort(compareEntries);
55
+ const kept = sorted.slice(0, maxEnvelopes);
56
+ const cappedTail = sorted.slice(maxEnvelopes);
57
+ const droppedForCap = cappedTail.map((e) => e.envelopeId);
58
+ const oversizeCount = kept.reduce((acc, e) => acc + (e.oversize ? 1 : 0), 0);
59
+ return {
60
+ entries: kept,
61
+ droppedForDuplicate,
62
+ droppedForCap,
63
+ oversizeCount,
64
+ };
65
+ };
@@ -0,0 +1,39 @@
1
+ import type { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
2
+ type Envelope = QualityIntelligence.QualityIntelligenceSourceEnvelope;
3
+ type EnvelopeId = QualityIntelligence.QualityIntelligenceSourceEnvelopeId;
4
+ /** A logical group of envelopes (e.g. one Conversation Center thread, one repo scan). */
5
+ export interface SourceGroup {
6
+ /** Stable label for the group; used as the provenance origin. */
7
+ readonly groupLabel: string;
8
+ readonly envelopes: readonly Envelope[];
9
+ }
10
+ export interface ProvenanceEntry {
11
+ readonly envelopeId: EnvelopeId;
12
+ /** First group label that contributed this envelope. */
13
+ readonly firstGroupLabel: string;
14
+ /** All distinct group labels that contributed this envelope (insertion-stable). */
15
+ readonly contributingGroupLabels: readonly string[];
16
+ }
17
+ export interface ReconciledSourceSet {
18
+ /** Distinct envelopes in encounter order across the input groups. */
19
+ readonly envelopes: readonly Envelope[];
20
+ /** One provenance entry per distinct envelope id. */
21
+ readonly provenance: readonly ProvenanceEntry[];
22
+ /** Envelope ids that appeared in more than one group. */
23
+ readonly duplicatedAcrossGroups: readonly EnvelopeId[];
24
+ /** Envelopes that were skipped because the same id appeared with mismatched kind. */
25
+ readonly conflictingEnvelopeIds: readonly EnvelopeId[];
26
+ }
27
+ /**
28
+ * Merge multiple envelope groups into a single non-overlapping set. Pure.
29
+ *
30
+ * Invariants:
31
+ * * Order: first appearance wins (encounter order across groups, then within group).
32
+ * * Conflict: same id with different kind = both contributors are recorded in
33
+ * `conflictingEnvelopeIds` and neither appears in `envelopes`.
34
+ * * Duplicate: same id with same kind = first envelope kept; later groups appear in
35
+ * the provenance entry's `contributingGroupLabels`.
36
+ */
37
+ export declare const reconcileSourceGroups: (groups: readonly SourceGroup[]) => ReconciledSourceSet;
38
+ export {};
39
+ //# sourceMappingURL=sourceReconciliation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sourceReconciliation.d.ts","sourceRoot":"","sources":["../../src/ingestion/sourceReconciliation.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,KAAK,QAAQ,GAAG,mBAAmB,CAAC,iCAAiC,CAAC;AACtE,KAAK,UAAU,GAAG,mBAAmB,CAAC,mCAAmC,CAAC;AAE1E,yFAAyF;AACzF,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,mFAAmF;IACnF,QAAQ,CAAC,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrD;AAED,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,qDAAqD;IACrD,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;IAChD,yDAAyD;IACzD,QAAQ,CAAC,sBAAsB,EAAE,SAAS,UAAU,EAAE,CAAC;IACvD,qFAAqF;IACrF,QAAQ,CAAC,sBAAsB,EAAE,SAAS,UAAU,EAAE,CAAC;CACxD;AA8BD;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAAI,QAAQ,SAAS,WAAW,EAAE,KAAG,mBAuCtE,CAAC"}
@@ -0,0 +1,74 @@
1
+ // Quality Intelligence — source reconciliation (Epic #270, Issue #278).
2
+ //
3
+ // Deterministic merge of multiple envelope groups (e.g. requirements + Figma + repo
4
+ // context) into a single non-overlapping `ReconciledSourceSet` with provenance preserved
5
+ // per envelope id.
6
+ //
7
+ // Pure: no IO, no clock, no randomness. Operates only on contract types from
8
+ // @oscharko-dev/keiko-contracts.
9
+ //
10
+ // Structurally inspired by Test Intelligence reference (TI) source-reconciliation
11
+ // patterns, but the provenance shape is anchored on the Keiko contracts surface.
12
+ const indexEnvelope = (envelope, groupLabel, byId, provById, conflicts, duplicates) => {
13
+ const existing = byId.get(envelope.id);
14
+ if (existing === undefined) {
15
+ byId.set(envelope.id, envelope);
16
+ provById.set(envelope.id, {
17
+ firstGroupLabel: groupLabel,
18
+ contributingGroupLabels: [groupLabel],
19
+ });
20
+ return;
21
+ }
22
+ if (existing.kind !== envelope.kind) {
23
+ conflicts.add(envelope.id);
24
+ return;
25
+ }
26
+ duplicates.add(envelope.id);
27
+ const prov = provById.get(envelope.id);
28
+ if (prov !== undefined && !prov.contributingGroupLabels.includes(groupLabel)) {
29
+ prov.contributingGroupLabels.push(groupLabel);
30
+ }
31
+ };
32
+ /**
33
+ * Merge multiple envelope groups into a single non-overlapping set. Pure.
34
+ *
35
+ * Invariants:
36
+ * * Order: first appearance wins (encounter order across groups, then within group).
37
+ * * Conflict: same id with different kind = both contributors are recorded in
38
+ * `conflictingEnvelopeIds` and neither appears in `envelopes`.
39
+ * * Duplicate: same id with same kind = first envelope kept; later groups appear in
40
+ * the provenance entry's `contributingGroupLabels`.
41
+ */
42
+ export const reconcileSourceGroups = (groups) => {
43
+ const byId = new Map();
44
+ const provById = new Map();
45
+ const conflicts = new Set();
46
+ const duplicates = new Set();
47
+ for (const group of groups) {
48
+ for (const envelope of group.envelopes) {
49
+ indexEnvelope(envelope, group.groupLabel, byId, provById, conflicts, duplicates);
50
+ }
51
+ }
52
+ for (const id of conflicts) {
53
+ byId.delete(id);
54
+ provById.delete(id);
55
+ duplicates.delete(id);
56
+ }
57
+ const envelopes = [];
58
+ const provenance = [];
59
+ for (const [id, envelope] of byId) {
60
+ const prov = provById.get(id);
61
+ envelopes.push(envelope);
62
+ provenance.push({
63
+ envelopeId: id,
64
+ firstGroupLabel: prov?.firstGroupLabel ?? "",
65
+ contributingGroupLabels: prov?.contributingGroupLabels ?? [],
66
+ });
67
+ }
68
+ return {
69
+ envelopes,
70
+ provenance,
71
+ duplicatedAcrossGroups: [...duplicates],
72
+ conflictingEnvelopeIds: [...conflicts],
73
+ };
74
+ };
@@ -0,0 +1,23 @@
1
+ /** Options governing clamp behaviour. */
2
+ export interface NormaliseUntrustedContentOptions {
3
+ /** Maximum UTF-8 byte length to retain. Defaults to 64 KiB. */
4
+ readonly maxBytes?: number;
5
+ }
6
+ export interface NormaliseUntrustedContentResult {
7
+ readonly value: string;
8
+ readonly clamped: boolean;
9
+ readonly normalisedFromControlChars: boolean;
10
+ readonly markdownInjectionEscapes: number;
11
+ }
12
+ /**
13
+ * Normalise an untrusted free-text payload:
14
+ * 1. NFKC normalise.
15
+ * 2. Strip C0/C1/DEL control characters (keeping TAB/LF/CR text whitespace).
16
+ * 3. Escape Markdown-injection vectors (heading, fenced code, image, link).
17
+ * 4. Clamp to `maxBytes` UTF-8 bytes (default 64 KiB).
18
+ *
19
+ * Pure: no IO, no clock, no randomness.
20
+ */
21
+ export declare const normaliseUntrustedContent: (raw: string, options?: NormaliseUntrustedContentOptions) => NormaliseUntrustedContentResult;
22
+ export declare const UNTRUSTED_CONTENT_DEFAULT_MAX_BYTES: number;
23
+ //# sourceMappingURL=untrustedContentNormalisation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"untrustedContentNormalisation.d.ts","sourceRoot":"","sources":["../../src/ingestion/untrustedContentNormalisation.ts"],"names":[],"mappings":"AAgBA,yCAAyC;AACzC,MAAM,WAAW,gCAAgC;IAC/C,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,0BAA0B,EAAE,OAAO,CAAC;IAC7C,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC;CAC3C;AA2FD;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,GACpC,KAAK,MAAM,EACX,UAAS,gCAAqC,KAC7C,+BAYF,CAAC;AAEF,eAAO,MAAM,mCAAmC,QAAoB,CAAC"}
@@ -0,0 +1,121 @@
1
+ // Quality Intelligence — untrusted content normalisation (Epic #270, Issue #278).
2
+ //
3
+ // Pure deterministic primitives that prepare free-text payloads (Figma node text,
4
+ // connector document snippets, human-context strings) for safe transit through the
5
+ // QI evidence-atom + envelope contracts. No IO. No network. No `node:fs`. No clock
6
+ // reads. No randomness. The functions never throw on inputs they choose to clamp —
7
+ // callers may layer a typed error on top.
8
+ //
9
+ // Structurally inspired by Test Intelligence reference (TI) multi-source text
10
+ // normalisation pipelines, but rewritten to consume the Keiko contracts surface and
11
+ // match the audit-ledger's NFKC + control-char rules already encoded in
12
+ // `validateQualityIntelligenceIdString` (@oscharko-dev/keiko-contracts, ids.ts).
13
+ const DEFAULT_MAX_BYTES = 64 * 1024;
14
+ const CLAMP_SUFFIX = "…";
15
+ // Markdown-injection vectors we escape (not strip) so the audit ledger retains the
16
+ // surrounding text. Escape with a leading backslash; the rendered text is byte-stable.
17
+ const HEADING_LINE = /^(#{1,6})/gmu;
18
+ const FENCED_CODE = /```/gu;
19
+ const IMAGE_OPEN = /!\[/gu;
20
+ const LINK_OPEN = /(?<!!)\[([^\]]*)\]\(/gu;
21
+ const isControlCodePoint = (code) => {
22
+ // TAB (0x09), LF (0x0A), and CR (0x0D) are C0 code points but are legitimate
23
+ // text whitespace: stripping them would collapse multi-line content (e.g. a
24
+ // code block or a multi-line document excerpt) into a single run-on line. They
25
+ // must survive normalisation. This mirrors the prompt-boundary scrubber and the
26
+ // single-file binary guard (keiko-server isControlChar), so every layer treats
27
+ // the same code points as text.
28
+ if (code === 0x09 || code === 0x0a || code === 0x0d)
29
+ return false;
30
+ // C0 controls 0x00–0x1F, DEL 0x7F, C1 controls 0x80–0x9F.
31
+ if (code <= 0x1f)
32
+ return true;
33
+ if (code === 0x7f)
34
+ return true;
35
+ if (code >= 0x80 && code <= 0x9f)
36
+ return true;
37
+ return false;
38
+ };
39
+ const stripControlCharacters = (value) => {
40
+ let out = "";
41
+ let stripped = false;
42
+ for (let i = 0; i < value.length; i += 1) {
43
+ const code = value.charCodeAt(i);
44
+ if (isControlCodePoint(code)) {
45
+ stripped = true;
46
+ continue;
47
+ }
48
+ out += value.charAt(i);
49
+ }
50
+ return { value: out, stripped };
51
+ };
52
+ const escapeMarkdownInjection = (value) => {
53
+ let count = 0;
54
+ const headingEscaped = value.replace(HEADING_LINE, (hashes) => {
55
+ count += 1;
56
+ return `\\${hashes}`;
57
+ });
58
+ const codeEscaped = headingEscaped.replace(FENCED_CODE, () => {
59
+ count += 1;
60
+ return "\\`\\`\\`";
61
+ });
62
+ const imageEscaped = codeEscaped.replace(IMAGE_OPEN, () => {
63
+ count += 1;
64
+ return "\\!\\[";
65
+ });
66
+ const linkEscaped = imageEscaped.replace(LINK_OPEN, (_match, inner) => {
67
+ count += 1;
68
+ return `\\[${inner}\\](`;
69
+ });
70
+ return { value: linkEscaped, count };
71
+ };
72
+ const clampToBytes = (value, maxBytes) => {
73
+ if (maxBytes <= 0) {
74
+ return { value: "", clamped: value.length > 0 };
75
+ }
76
+ const encoder = new TextEncoder();
77
+ const bytes = encoder.encode(value);
78
+ if (bytes.length <= maxBytes) {
79
+ return { value, clamped: false };
80
+ }
81
+ let truncatedBytes = bytes.slice(0, maxBytes);
82
+ // Trim trailing partial UTF-8 sequence (continuation bytes 0x80–0xBF without a starter).
83
+ while (truncatedBytes.length > 0) {
84
+ const last = truncatedBytes[truncatedBytes.length - 1] ?? 0;
85
+ if ((last & 0xc0) === 0x80) {
86
+ truncatedBytes = truncatedBytes.slice(0, -1);
87
+ }
88
+ else {
89
+ // If this is itself a multi-byte starter (>= 0xC0), drop it too: it has no continuation.
90
+ if (last >= 0xc0) {
91
+ truncatedBytes = truncatedBytes.slice(0, -1);
92
+ }
93
+ break;
94
+ }
95
+ }
96
+ const decoder = new TextDecoder("utf-8", { fatal: false });
97
+ return { value: `${decoder.decode(truncatedBytes)}${CLAMP_SUFFIX}`, clamped: true };
98
+ };
99
+ /**
100
+ * Normalise an untrusted free-text payload:
101
+ * 1. NFKC normalise.
102
+ * 2. Strip C0/C1/DEL control characters (keeping TAB/LF/CR text whitespace).
103
+ * 3. Escape Markdown-injection vectors (heading, fenced code, image, link).
104
+ * 4. Clamp to `maxBytes` UTF-8 bytes (default 64 KiB).
105
+ *
106
+ * Pure: no IO, no clock, no randomness.
107
+ */
108
+ export const normaliseUntrustedContent = (raw, options = {}) => {
109
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
110
+ const normalised = raw.normalize("NFKC");
111
+ const stripped = stripControlCharacters(normalised);
112
+ const escaped = escapeMarkdownInjection(stripped.value);
113
+ const clamped = clampToBytes(escaped.value, maxBytes);
114
+ return {
115
+ value: clamped.value,
116
+ clamped: clamped.clamped,
117
+ normalisedFromControlChars: stripped.stripped,
118
+ markdownInjectionEscapes: escaped.count,
119
+ };
120
+ };
121
+ export const UNTRUSTED_CONTENT_DEFAULT_MAX_BYTES = DEFAULT_MAX_BYTES;
@@ -0,0 +1,55 @@
1
+ import type { ContextPack } from "@oscharko-dev/keiko-contracts";
2
+ import { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
3
+ import { type SourceMixPlan } from "./sourceMixPlanning.js";
4
+ type RepoEnvelope = QualityIntelligence.QualityIntelligenceRepositoryContextEnvelope;
5
+ export type WorkspaceAdapterErrorCode = "ABSOLUTE_PATH" | "PATH_TRAVERSAL" | "EMPTY_PATH" | "INVALID_INTEGRITY_HASH" | "INVALID_REGISTERED_AT";
6
+ export declare class WorkspaceAdapterError extends Error {
7
+ readonly code: WorkspaceAdapterErrorCode;
8
+ constructor(code: WorkspaceAdapterErrorCode, message: string);
9
+ }
10
+ export interface BuildWorkspaceEnvelopesInput {
11
+ /**
12
+ * Display-only label naming the workspace (e.g. "main", "feature/foo"). Not a path,
13
+ * not a URL; carried only into envelope `displayLabel` and `provenance.origin`.
14
+ */
15
+ readonly workspaceLabel: string;
16
+ /**
17
+ * ISO 8601 UTC timestamp the consumer captured when assembling the pack. The QI
18
+ * ingestion sub-namespace deliberately refuses to read the clock so callers stay
19
+ * deterministic.
20
+ */
21
+ readonly registeredAt: string;
22
+ readonly contextPack: ContextPack;
23
+ /**
24
+ * Caller-supplied SHA-256 hex digest table keyed by the context-entry path. The
25
+ * adapter rejects any entry without a matching digest with `INVALID_INTEGRITY_HASH`.
26
+ */
27
+ readonly integrityHashByEntryPath: Readonly<Record<string, string>>;
28
+ /**
29
+ * Optional id-prefix the caller pre-allocates (e.g. "qi-src-ws-1234"). Each envelope's
30
+ * id is `${idPrefix}:${entry.path}`; the contract validator (asQualityIntelligence-
31
+ * SourceEnvelopeId) rejects forbidden fragments — callers must pre-validate the
32
+ * prefix.
33
+ */
34
+ readonly idPrefix: string;
35
+ }
36
+ /**
37
+ * Convert a workspace `ContextPack` into a list of repository-context source envelopes,
38
+ * one per context entry. Pure, no IO.
39
+ *
40
+ * Rejects:
41
+ * * absolute paths or paths containing `..` segments
42
+ * * missing or non-hex-64 integrity hashes
43
+ * * malformed `registeredAt` timestamps
44
+ */
45
+ export declare const buildWorkspaceSourceEnvelopes: (input: BuildWorkspaceEnvelopesInput) => readonly RepoEnvelope[];
46
+ /**
47
+ * Convenience: build envelopes AND run the deterministic mix planner so the consumer
48
+ * gets the policy-ordered planner output in a single call. Pure.
49
+ */
50
+ export declare const workspaceSourceMixPolicy: (input: BuildWorkspaceEnvelopesInput) => {
51
+ readonly envelopes: readonly RepoEnvelope[];
52
+ readonly plan: SourceMixPlan;
53
+ };
54
+ export {};
55
+ //# sourceMappingURL=workspaceAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspaceAdapter.d.ts","sourceRoot":"","sources":["../../src/ingestion/workspaceAdapter.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE3E,KAAK,YAAY,GAAG,mBAAmB,CAAC,4CAA4C,CAAC;AAGrF,MAAM,MAAM,yBAAyB,GACjC,eAAe,GACf,gBAAgB,GAChB,YAAY,GACZ,wBAAwB,GACxB,uBAAuB,CAAC;AAE5B,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,SAAgB,IAAI,EAAE,yBAAyB,CAAC;gBACpC,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,MAAM;CAK7D;AAyDD,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACxC,OAAO,4BAA4B,KAClC,SAAS,YAAY,EA2BvB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GACnC,OAAO,4BAA4B,KAClC;IACD,QAAQ,CAAC,SAAS,EAAE,SAAS,YAAY,EAAE,CAAC;IAC5C,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;CAK9B,CAAC"}
@@ -0,0 +1,113 @@
1
+ // Quality Intelligence — Workspace adapter (Epic #270, Issue #278).
2
+ //
3
+ // Pure adapter that converts a `ContextPack` from @oscharko-dev/keiko-workspace into a
4
+ // `QualityIntelligenceRepositoryContextEnvelope` (kind: "repository-context") suitable
5
+ // for QI ingestion. The QI pure-domain package is locked down (ADR-0019 rule 10a) to
6
+ // `keiko-contracts` and `keiko-security` only, so this adapter consumes the workspace
7
+ // contract types via their re-export from @oscharko-dev/keiko-contracts (workspace.ts).
8
+ //
9
+ // Why this is safe:
10
+ // * Path containment stays in @oscharko-dev/keiko-workspace at construction time.
11
+ // * The adapter accepts only the already-redacted `ContextPack` and carries only
12
+ // workspace-relative refs into the envelope. Absolute paths and `..` segments are
13
+ // rejected with a typed `WorkspaceAdapterError` so a caller cannot smuggle an
14
+ // out-of-workspace ref through the contract surface.
15
+ // * The envelope `integrityHashSha256Hex` is computed by the caller (the workspace
16
+ // adapter declines to import @oscharko-dev/keiko-security to avoid pulling a hashing
17
+ // primitive into the QI ingestion sub-namespace; callers compute the digest in the
18
+ // keiko-workspace consumer and pass it in).
19
+ //
20
+ // Structurally inspired by Test Intelligence reference (TI) repo-context adapters, but
21
+ // the envelope shape is anchored on the QI contracts.
22
+ import { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
23
+ import { planSourceMix } from "./sourceMixPlanning.js";
24
+ const { asQualityIntelligenceSourceEnvelopeId } = QualityIntelligence;
25
+ export class WorkspaceAdapterError extends Error {
26
+ code;
27
+ constructor(code, message) {
28
+ super(`[${code}] ${message}`);
29
+ this.name = "WorkspaceAdapterError";
30
+ this.code = code;
31
+ }
32
+ }
33
+ const HEX64 = /^[0-9a-f]{64}$/u;
34
+ const ISO_8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/u;
35
+ const assertRelativePath = (path) => {
36
+ if (path.length === 0) {
37
+ throw new WorkspaceAdapterError("EMPTY_PATH", "Context entry path is empty");
38
+ }
39
+ // Absolute path on POSIX or Windows drive-style ("C:\…" / "C:/…").
40
+ if (path.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(path)) {
41
+ throw new WorkspaceAdapterError("ABSOLUTE_PATH", `Context entry path must be workspace-relative; got "${path}"`);
42
+ }
43
+ // Reject any `..` segment regardless of separator.
44
+ const segments = path.split(/[\\/]/u);
45
+ if (segments.some((s) => s === "..")) {
46
+ throw new WorkspaceAdapterError("PATH_TRAVERSAL", `Context entry path contains a ".." segment: "${path}"`);
47
+ }
48
+ };
49
+ const assertHash = (hash) => {
50
+ if (!HEX64.test(hash)) {
51
+ throw new WorkspaceAdapterError("INVALID_INTEGRITY_HASH", "integrityHashSha256Hex must be 64 lowercase hex chars");
52
+ }
53
+ };
54
+ const assertRegisteredAt = (timestamp) => {
55
+ if (!ISO_8601.test(timestamp)) {
56
+ throw new WorkspaceAdapterError("INVALID_REGISTERED_AT", "registeredAt must be ISO 8601 UTC (e.g. 2026-06-05T00:00:00Z)");
57
+ }
58
+ };
59
+ const buildLocalRef = (entry) => {
60
+ assertRelativePath(entry.path);
61
+ return entry.path;
62
+ };
63
+ const buildDisplayLabel = (rootLabel, entry) => {
64
+ const base = `${rootLabel}:${entry.path}`;
65
+ // The envelope itself caps display labels at 256; we trim here so the consumer
66
+ // never has to discard an otherwise valid envelope.
67
+ if (base.length <= 256)
68
+ return base;
69
+ return `${base.slice(0, 253)}...`;
70
+ };
71
+ /**
72
+ * Convert a workspace `ContextPack` into a list of repository-context source envelopes,
73
+ * one per context entry. Pure, no IO.
74
+ *
75
+ * Rejects:
76
+ * * absolute paths or paths containing `..` segments
77
+ * * missing or non-hex-64 integrity hashes
78
+ * * malformed `registeredAt` timestamps
79
+ */
80
+ export const buildWorkspaceSourceEnvelopes = (input) => {
81
+ assertRegisteredAt(input.registeredAt);
82
+ const envelopes = [];
83
+ for (const entry of input.contextPack.selected) {
84
+ const localRef = buildLocalRef(entry);
85
+ const hash = input.integrityHashByEntryPath[entry.path];
86
+ if (typeof hash !== "string") {
87
+ throw new WorkspaceAdapterError("INVALID_INTEGRITY_HASH", `No integrity hash supplied for entry "${entry.path}"`);
88
+ }
89
+ assertHash(hash);
90
+ const id = asQualityIntelligenceSourceEnvelopeId(`${input.idPrefix}:${entry.path}`);
91
+ envelopes.push({
92
+ id,
93
+ kind: "repository-context",
94
+ displayLabel: buildDisplayLabel(input.workspaceLabel, entry),
95
+ provenance: {
96
+ origin: `workspace:${input.workspaceLabel}`,
97
+ registeredAt: input.registeredAt,
98
+ integrityHashSha256Hex: hash,
99
+ },
100
+ localRef,
101
+ });
102
+ }
103
+ return envelopes;
104
+ };
105
+ /**
106
+ * Convenience: build envelopes AND run the deterministic mix planner so the consumer
107
+ * gets the policy-ordered planner output in a single call. Pure.
108
+ */
109
+ export const workspaceSourceMixPolicy = (input) => {
110
+ const envelopes = buildWorkspaceSourceEnvelopes(input);
111
+ const plan = planSourceMix(envelopes);
112
+ return { envelopes, plan };
113
+ };
@@ -0,0 +1,61 @@
1
+ import type { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
2
+ import type { NextReviewState, QualityIntelligenceReviewTransitionEvent } from "./stateMachine.js";
3
+ export declare const QUALITY_INTELLIGENCE_REVIEW_AUDIT_EVENT_SCHEMA_VERSION: 1;
4
+ export type QualityIntelligenceReviewAuditEventKind = "qi:review:opened" | "qi:review:transitioned" | "qi:review:four-eyes-paired" | "qi:review:terminated";
5
+ export declare const QUALITY_INTELLIGENCE_REVIEW_AUDIT_EVENT_KINDS: readonly QualityIntelligenceReviewAuditEventKind[];
6
+ export interface QualityIntelligenceReviewOpenedPayload {
7
+ readonly kind: "qi:review:opened";
8
+ readonly reviewerKind: QualityIntelligence.QualityIntelligenceReviewerKind;
9
+ }
10
+ export interface QualityIntelligenceReviewTransitionedPayload {
11
+ readonly kind: "qi:review:transitioned";
12
+ readonly from: QualityIntelligence.QualityIntelligenceReviewState;
13
+ readonly to: QualityIntelligence.QualityIntelligenceReviewState;
14
+ readonly event: QualityIntelligenceReviewTransitionEvent;
15
+ }
16
+ export interface QualityIntelligenceReviewFourEyesPairedPayload {
17
+ readonly kind: "qi:review:four-eyes-paired";
18
+ readonly pairedRecordId: QualityIntelligence.QualityIntelligenceReviewRecordId;
19
+ }
20
+ export interface QualityIntelligenceReviewTerminatedPayload {
21
+ readonly kind: "qi:review:terminated";
22
+ readonly terminalState: QualityIntelligence.QualityIntelligenceReviewState;
23
+ }
24
+ export type QualityIntelligenceReviewAuditEventPayload = QualityIntelligenceReviewOpenedPayload | QualityIntelligenceReviewTransitionedPayload | QualityIntelligenceReviewFourEyesPairedPayload | QualityIntelligenceReviewTerminatedPayload;
25
+ export interface QualityIntelligenceReviewAuditEvent {
26
+ readonly eventSchemaVersion: typeof QUALITY_INTELLIGENCE_REVIEW_AUDIT_EVENT_SCHEMA_VERSION;
27
+ readonly runId: QualityIntelligence.QualityIntelligenceRunId;
28
+ readonly recordId: QualityIntelligence.QualityIntelligenceReviewRecordId;
29
+ /** Monotonic per-run non-negative integer. Assigned by the runtime. */
30
+ readonly sequence: number;
31
+ /** ISO 8601 timestamp supplied by the caller. */
32
+ readonly timestamp: string;
33
+ /** Display-only actor label. NO PII guarantee — same rule as reviewerLabel. */
34
+ readonly by: string;
35
+ readonly payload: QualityIntelligenceReviewAuditEventPayload;
36
+ }
37
+ export interface BuildReviewAuditEventInput {
38
+ readonly runId: QualityIntelligence.QualityIntelligenceRunId;
39
+ readonly recordId: QualityIntelligence.QualityIntelligenceReviewRecordId;
40
+ readonly sequence: number;
41
+ readonly timestamp: string;
42
+ readonly by: string;
43
+ readonly payload: QualityIntelligenceReviewAuditEventPayload;
44
+ }
45
+ /**
46
+ * Build a frozen audit-event envelope. Pure: does not consult any clock, does
47
+ * not allocate beyond the returned envelope, does not validate `timestamp`
48
+ * beyond presence. The runtime (#273) assigns `sequence` in observed order;
49
+ * callers should pass the next integer for the run.
50
+ *
51
+ * Throws `RangeError` if `sequence` is not a non-negative integer — this is
52
+ * the same guarantee as `assertRunEventSequenceMonotonic`.
53
+ */
54
+ export declare const buildReviewAuditEvent: (input: BuildReviewAuditEventInput) => QualityIntelligenceReviewAuditEvent;
55
+ /**
56
+ * Convenience derivation: build the `qi:review:transitioned` payload from a
57
+ * `NextReviewState` produced by `applyReviewTransition`. Exposed so the
58
+ * runtime can compose the two pure functions without re-deriving fields.
59
+ */
60
+ export declare const transitionedPayloadFromNext: (next: NextReviewState) => QualityIntelligenceReviewTransitionedPayload;
61
+ //# sourceMappingURL=auditEvents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auditEvents.d.ts","sourceRoot":"","sources":["../../src/review/auditEvents.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,wCAAwC,EAAE,MAAM,mBAAmB,CAAC;AAEnG,eAAO,MAAM,sDAAsD,EAAG,CAAU,CAAC;AAEjF,MAAM,MAAM,uCAAuC,GAC/C,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,sBAAsB,CAAC;AAE3B,eAAO,MAAM,6CAA6C,EAAE,SAAS,uCAAuC,EAMhG,CAAC;AAEb,MAAM,WAAW,sCAAsC;IACrD,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC,+BAA+B,CAAC;CAC5E;AAED,MAAM,WAAW,4CAA4C;IAC3D,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC,8BAA8B,CAAC;IAClE,QAAQ,CAAC,EAAE,EAAE,mBAAmB,CAAC,8BAA8B,CAAC;IAChE,QAAQ,CAAC,KAAK,EAAE,wCAAwC,CAAC;CAC1D;AAED,MAAM,WAAW,8CAA8C;IAC7D,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,cAAc,EAAE,mBAAmB,CAAC,iCAAiC,CAAC;CAChF;AAED,MAAM,WAAW,0CAA0C;IACzD,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC,8BAA8B,CAAC;CAC5E;AAED,MAAM,MAAM,0CAA0C,GAClD,sCAAsC,GACtC,4CAA4C,GAC5C,8CAA8C,GAC9C,0CAA0C,CAAC;AAE/C,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,kBAAkB,EAAE,OAAO,sDAAsD,CAAC;IAC3F,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAC,wBAAwB,CAAC;IAC7D,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC,iCAAiC,CAAC;IACzE,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,0CAA0C,CAAC;CAC9D;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAC,wBAAwB,CAAC;IAC7D,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC,iCAAiC,CAAC;IACzE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,0CAA0C,CAAC;CAC9D;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAChC,OAAO,0BAA0B,KAChC,mCAeF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,GACtC,MAAM,eAAe,KACpB,4CAMC,CAAC"}
@@ -0,0 +1,50 @@
1
+ // Quality Intelligence review audit-event producer (Epic #270, Issue #282).
2
+ //
3
+ // Pure-domain envelope builder. Emits versioned audit events for the review
4
+ // lifecycle. PRODUCTION ONLY — persistence belongs to keiko-evidence (#274)
5
+ // and the audit ledger. No IO, no clock, no allocations beyond the returned
6
+ // frozen envelope.
7
+ //
8
+ // Schema-version policy mirrors QUALITY_INTELLIGENCE_EVENT_SCHEMA_VERSION:
9
+ // future evolutions land as new numeric literals, never as a mutation of "1".
10
+ export const QUALITY_INTELLIGENCE_REVIEW_AUDIT_EVENT_SCHEMA_VERSION = 1;
11
+ export const QUALITY_INTELLIGENCE_REVIEW_AUDIT_EVENT_KINDS = [
12
+ "qi:review:opened",
13
+ "qi:review:transitioned",
14
+ "qi:review:four-eyes-paired",
15
+ "qi:review:terminated",
16
+ ];
17
+ /**
18
+ * Build a frozen audit-event envelope. Pure: does not consult any clock, does
19
+ * not allocate beyond the returned envelope, does not validate `timestamp`
20
+ * beyond presence. The runtime (#273) assigns `sequence` in observed order;
21
+ * callers should pass the next integer for the run.
22
+ *
23
+ * Throws `RangeError` if `sequence` is not a non-negative integer — this is
24
+ * the same guarantee as `assertRunEventSequenceMonotonic`.
25
+ */
26
+ export const buildReviewAuditEvent = (input) => {
27
+ if (!Number.isInteger(input.sequence) || input.sequence < 0) {
28
+ throw new RangeError(`Review audit event sequence must be a non-negative integer, got ${String(input.sequence)}`);
29
+ }
30
+ return Object.freeze({
31
+ eventSchemaVersion: QUALITY_INTELLIGENCE_REVIEW_AUDIT_EVENT_SCHEMA_VERSION,
32
+ runId: input.runId,
33
+ recordId: input.recordId,
34
+ sequence: input.sequence,
35
+ timestamp: input.timestamp,
36
+ by: input.by,
37
+ payload: input.payload,
38
+ });
39
+ };
40
+ /**
41
+ * Convenience derivation: build the `qi:review:transitioned` payload from a
42
+ * `NextReviewState` produced by `applyReviewTransition`. Exposed so the
43
+ * runtime can compose the two pure functions without re-deriving fields.
44
+ */
45
+ export const transitionedPayloadFromNext = (next) => Object.freeze({
46
+ kind: "qi:review:transitioned",
47
+ from: next.from,
48
+ to: next.state,
49
+ event: next.event,
50
+ });
@@ -0,0 +1,24 @@
1
+ import type { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
2
+ export type QualityIntelligenceFourEyesViolationCode = "SELF_REVIEW_FORBIDDEN" | "SAME_REVIEWER_LABEL" | "ALREADY_PAIRED";
3
+ export declare class QualityIntelligenceFourEyesViolationError extends Error {
4
+ readonly code: QualityIntelligenceFourEyesViolationCode;
5
+ readonly recordId: QualityIntelligence.QualityIntelligenceReviewRecordId;
6
+ readonly candidateId: QualityIntelligence.QualityIntelligenceReviewRecordId;
7
+ constructor(code: QualityIntelligenceFourEyesViolationCode, record: QualityIntelligence.QualityIntelligenceReviewRecord, candidate: QualityIntelligence.QualityIntelligenceReviewRecord, detail: string);
8
+ }
9
+ /**
10
+ * Assert that `record` and `candidate` may be paired for four-eyes governance.
11
+ * Throws a typed `QualityIntelligenceFourEyesViolationError` on:
12
+ *
13
+ * * SELF_REVIEW_FORBIDDEN — both records have `reviewerKind === "human-author"`.
14
+ * A human author may not also be the four-eyes second reviewer of their own
15
+ * authored output.
16
+ * * SAME_REVIEWER_LABEL — both records share the same (trimmed, case-folded)
17
+ * `reviewerLabel`. Display-only string comparison; no identity guarantee.
18
+ * * ALREADY_PAIRED — either record already references a `fourEyesPairedRecordId`.
19
+ *
20
+ * Returns `void` on success. The caller is responsible for actually wiring
21
+ * `fourEyesPairedRecordId` on both records when pairing succeeds.
22
+ */
23
+ export declare const assertFourEyesPair: (record: QualityIntelligence.QualityIntelligenceReviewRecord, candidate: QualityIntelligence.QualityIntelligenceReviewRecord) => void;
24
+ //# sourceMappingURL=fourEyes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fourEyes.d.ts","sourceRoot":"","sources":["../../src/review/fourEyes.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,MAAM,MAAM,wCAAwC,GAChD,uBAAuB,GACvB,qBAAqB,GACrB,gBAAgB,CAAC;AAErB,qBAAa,yCAA0C,SAAQ,KAAK;IAClE,SAAgB,IAAI,EAAE,wCAAwC,CAAC;IAC/D,SAAgB,QAAQ,EAAE,mBAAmB,CAAC,iCAAiC,CAAC;IAChF,SAAgB,WAAW,EAAE,mBAAmB,CAAC,iCAAiC,CAAC;gBAGjF,IAAI,EAAE,wCAAwC,EAC9C,MAAM,EAAE,mBAAmB,CAAC,+BAA+B,EAC3D,SAAS,EAAE,mBAAmB,CAAC,+BAA+B,EAC9D,MAAM,EAAE,MAAM;CAQjB;AAID;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,mBAAmB,CAAC,+BAA+B,EAC3D,WAAW,mBAAmB,CAAC,+BAA+B,KAC7D,IA4BF,CAAC"}