@dailephd/my-dev-kit-lab 0.4.1 → 0.4.3

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 (103) hide show
  1. package/README.md +195 -12
  2. package/benchmarks/contracts/todo-behavior.md +1 -1
  3. package/benchmarks/projects/task-analytics-large-mixed/README.md +2 -0
  4. package/benchmarks/projects/task-workflow-medium-ts/README.md +3 -1
  5. package/dist/scripts/audits/runAudit.js +28 -1
  6. package/dist/src/audits/core/auditConfig.js +16 -1
  7. package/dist/src/audits/core/auditRunner.js +6 -2
  8. package/dist/src/audits/report/auditReportModel.js +7 -0
  9. package/dist/src/audits/report/renderAuditJsonReport.js +1 -0
  10. package/dist/src/audits/report/renderAuditTextReport.js +37 -0
  11. package/dist/src/audits/security/androidAuditIntegration.js +145 -0
  12. package/dist/src/audits/security/mapAndroidSecurityFindingToAuditIssue.js +116 -0
  13. package/dist/src/audits/security/mapSecurityFindingToAuditIssue.js +15 -5
  14. package/dist/src/audits/security/securityAuditAdapter.js +20 -2
  15. package/dist/src/audits/security/securityAuditTypes.js +37 -0
  16. package/dist/src/evaluation/stageContextDeterminism/calculateStageContextDeterminism.js +52 -0
  17. package/dist/src/evaluation/stageContextDeterminism/canonicalizeStageContextRun.js +53 -0
  18. package/dist/src/evaluation/stageContextDeterminism/index.js +2 -0
  19. package/dist/src/evaluation/stageContextDeterminism/types.js +1 -0
  20. package/dist/src/evaluation/stageContextExpectations/index.js +2 -0
  21. package/dist/src/evaluation/stageContextExpectations/readStageContextExpectationFixtureV1.js +50 -0
  22. package/dist/src/evaluation/stageContextExpectations/readTypes.js +1 -0
  23. package/dist/src/evaluation/stageContextExpectations/types.js +1 -0
  24. package/dist/src/evaluation/stageContextExpectations/validation.js +568 -0
  25. package/dist/src/evaluation/stageContextMetrics/calculateArtifactStateMetrics.js +130 -0
  26. package/dist/src/evaluation/stageContextMetrics/calculateContextSizeMetrics.js +85 -0
  27. package/dist/src/evaluation/stageContextMetrics/calculateExpectationMetrics.js +103 -0
  28. package/dist/src/evaluation/stageContextMetrics/calculateResponsibilityMappingMetrics.js +36 -0
  29. package/dist/src/evaluation/stageContextMetrics/collectContextCapsuleEvidence.js +312 -0
  30. package/dist/src/evaluation/stageContextMetrics/collectRetrievalAuditEvidence.js +35 -0
  31. package/dist/src/evaluation/stageContextMetrics/collectWorkflowInstructionEvidence.js +137 -0
  32. package/dist/src/evaluation/stageContextMetrics/evaluateV043StageContextExecution.js +247 -0
  33. package/dist/src/evaluation/stageContextMetrics/index.js +10 -0
  34. package/dist/src/evaluation/stageContextMetrics/matchStageContextExpectations.js +38 -0
  35. package/dist/src/evaluation/stageContextMetrics/targetKeys.js +35 -0
  36. package/dist/src/evaluation/stageContextMetrics/types.js +1 -0
  37. package/dist/src/evaluation/stageContextSelectors/contextArtifactConsistency.js +79 -0
  38. package/dist/src/evaluation/stageContextSelectors/contextCapsuleSelectors.js +126 -0
  39. package/dist/src/evaluation/stageContextSelectors/index.js +4 -0
  40. package/dist/src/evaluation/stageContextSelectors/retrievalAuditRecordSelectors.js +54 -0
  41. package/dist/src/evaluation/stageContextSelectors/workflowInstructionPacketSelectors.js +51 -0
  42. package/dist/src/evaluation/targetImmutability/captureTargetSnapshot.js +208 -0
  43. package/dist/src/evaluation/targetImmutability/compareTargetSnapshots.js +56 -0
  44. package/dist/src/evaluation/targetImmutability/index.js +3 -0
  45. package/dist/src/evaluation/targetImmutability/types.js +1 -0
  46. package/dist/src/evaluation/targetImmutability/validateTargetImmutabilityConfig.js +130 -0
  47. package/dist/src/evaluation/upstreamArtifacts/artifactReadTypes.js +1 -0
  48. package/dist/src/evaluation/upstreamArtifacts/index.js +3 -0
  49. package/dist/src/evaluation/upstreamArtifacts/jsonTypes.js +1 -0
  50. package/dist/src/evaluation/upstreamArtifacts/myDevKitClassificationTypesV1.js +1 -0
  51. package/dist/src/evaluation/upstreamArtifacts/myDevKitContextArtifactsV1.js +1 -0
  52. package/dist/src/evaluation/upstreamArtifacts/myDevKitSemanticTypesV1.js +1 -0
  53. package/dist/src/evaluation/upstreamArtifacts/orchestratorWorkflowInstructionPacketV1.js +1 -0
  54. package/dist/src/evaluation/upstreamArtifacts/readJsonArtifact.js +53 -0
  55. package/dist/src/evaluation/upstreamArtifacts/readMyDevKitContextCapsuleV1.js +9 -0
  56. package/dist/src/evaluation/upstreamArtifacts/readMyDevKitRetrievalAuditRecordV1.js +9 -0
  57. package/dist/src/evaluation/upstreamArtifacts/readOrchestratorWorkflowInstructionPacketV1.js +9 -0
  58. package/dist/src/evaluation/upstreamArtifacts/runtimeValidation.js +131 -0
  59. package/dist/src/evaluation/upstreamArtifacts/schemaVersion.js +32 -0
  60. package/dist/src/evaluation/upstreamArtifacts/validateMyDevKitContextCapsuleV1.js +1208 -0
  61. package/dist/src/evaluation/upstreamArtifacts/validateMyDevKitRetrievalAuditRecordV1.js +105 -0
  62. package/dist/src/evaluation/upstreamArtifacts/validateOrchestratorWorkflowInstructionPacketV1.js +235 -0
  63. package/dist/src/experiments/plugins/contextStrategyComparison/executeV043StageContextStrategy.js +46 -0
  64. package/dist/src/experiments/plugins/contextStrategyComparison/index.js +9 -0
  65. package/dist/src/experiments/plugins/contextStrategyComparison/loadV043StrategyArtifacts.js +286 -0
  66. package/dist/src/experiments/plugins/contextStrategyComparison/plugin.js +39 -1
  67. package/dist/src/experiments/plugins/contextStrategyComparison/readV043FullWorkflowLibraryFixture.js +162 -0
  68. package/dist/src/experiments/plugins/contextStrategyComparison/resolveV043StrategyInputs.js +43 -0
  69. package/dist/src/experiments/plugins/contextStrategyComparison/resultMapping.js +3 -0
  70. package/dist/src/experiments/plugins/contextStrategyComparison/runV043StageContextStrategyWithAssurance.js +190 -0
  71. package/dist/src/experiments/plugins/contextStrategyComparison/v043FullWorkflowLibraryFixture.js +1 -0
  72. package/dist/src/experiments/plugins/contextStrategyComparison/v043RunAssuranceTypes.js +1 -0
  73. package/dist/src/experiments/plugins/contextStrategyComparison/v043StrategyExecutionTypes.js +1 -0
  74. package/dist/src/experiments/plugins/contextStrategyComparison/v043StrategyIds.js +13 -0
  75. package/dist/src/experiments/plugins/contextStrategyComparison/v043StrategyInputContracts.js +1 -0
  76. package/dist/src/experiments/plugins/contextStrategyComparison/validateV043RunAssuranceConfig.js +59 -0
  77. package/dist/src/experiments/plugins/contextStrategyComparison/validateV043StrategyInput.js +252 -0
  78. package/dist/src/report/experiments/buildContextStrategyComparisonV043Report.js +398 -0
  79. package/dist/src/report/experiments/buildPluginExperimentReport.js +21 -6
  80. package/dist/src/report/experiments/contextStrategyComparisonV043ReportModel.js +1 -0
  81. package/dist/src/report/experiments/index.js +4 -0
  82. package/dist/src/report/experiments/renderContextStrategyComparisonV043Html.js +234 -0
  83. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  84. package/dist/src/report/experiments/renderPluginExperimentReportText.js +225 -0
  85. package/dist/src/report/experiments/writePluginExperimentReports.js +3 -0
  86. package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +215 -9
  87. package/docs/METRICS.md +56 -10
  88. package/package.json +1 -1
  89. package/tests/fixtures/full-workflow-library/complete-v1.0.0.json +24 -0
  90. package/tests/fixtures/full-workflow-library/future-minor-v1.1.0-additive.json +25 -0
  91. package/tests/fixtures/stage-context-expectations/complete-v1.0.0.json +230 -0
  92. package/tests/fixtures/stage-context-expectations/future-minor-v1.1.0-additive.json +232 -0
  93. package/tests/fixtures/stage-context-expectations/invalid/malformed-json.txt +1 -0
  94. package/tests/fixtures/stage-context-expectations/invalid/non-object-root.json +1 -0
  95. package/tests/fixtures/stage-context-expectations/minimal-v1.0.0.json +18 -0
  96. package/tests/fixtures/upstream-artifacts/invalid/malformed-json.txt +1 -0
  97. package/tests/fixtures/upstream-artifacts/invalid/non-object-root.json +1 -0
  98. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/context-capsule/complete-v1.0.0.json +504 -0
  99. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/context-capsule/future-minor-v1.1.0-additive.json +955 -0
  100. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/retrieval-audit-record/complete-v1.0.0.json +190 -0
  101. package/tests/fixtures/upstream-artifacts/my-dev-kit/1.10.2/retrieval-audit-record/future-minor-v1.1.0-additive.json +327 -0
  102. package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.1/workflow-instruction-packet/complete-v1.0.0.json +160 -0
  103. package/tests/fixtures/upstream-artifacts/my-dev-kit-orchestrator/1.2.1/workflow-instruction-packet/future-minor-v1.1.0-additive.json +186 -0
@@ -0,0 +1,116 @@
1
+ import { makeAuditEvidence } from "../core/auditIssue.js";
2
+ import { isBlockingSecuritySeverity, mapSecuritySeverityToAuditSeverity } from "./mapSecurityFindingToAuditIssue.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.4.2 Batch 1 — pure Android SecurityFinding -> AuditIssue mapper.
5
+ //
6
+ // Android checks already reuse SecurityFinding as-is (see
7
+ // src/mobile/android/audit/androidFinding.ts's own header comment) — there is
8
+ // no parallel "AndroidSecurityFinding" type. What Android findings lose by
9
+ // reusing the generic SecurityFinding shape is per-family taxonomy: every
10
+ // Android finding's own `category` is the single shared value "static-scan"
11
+ // (a SecurityCheckCategory), while the *specific* Android family (backup,
12
+ // secrets, webview, ...) only lives on the owning AndroidCheckResult.category.
13
+ // This mapper therefore takes a small AndroidAuditMappingContext alongside
14
+ // the finding — supplied by the (future Batch 2) caller that already has the
15
+ // owning AndroidCheckResult in hand — rather than trying to recover that
16
+ // context by parsing the finding's own opaque id/evidence text.
17
+ //
18
+ // Reuses mapSecurityFindingToAuditIssue's severity/blocking policy exactly
19
+ // (SecuritySeverity is the same shared type for Android and non-Android
20
+ // findings) instead of defining a second severity table. Pure, no I/O, no
21
+ // mutation, no CandidateEvidence handling — CandidateEvidence is a distinct
22
+ // type (see src/mobile/android/advancedSecurity/candidateEvidence.ts) and
23
+ // cannot be passed here at all: this function's parameter type is
24
+ // SecurityFinding, not CandidateEvidence.
25
+ // ---------------------------------------------------------------------------
26
+ export const ANDROID_SECURITY_AUDIT_DETECTOR_ID = "android-security-validation-adapter";
27
+ const DEFAULT_CONFIDENCE = "medium";
28
+ const DEFAULT_FALSE_POSITIVE_RISK = "medium";
29
+ // MobileConfidence carries an extra "unknown" state SecurityFinding-derived
30
+ // AuditConfidence has no equivalent for; mapped to the same conservative
31
+ // "medium" default the generic security mapper uses when no per-finding
32
+ // confidence exists at all, rather than inventing a fourth AuditConfidence
33
+ // value.
34
+ function mapCheckConfidenceToAuditConfidence(confidence) {
35
+ if (confidence === "high" || confidence === "medium" || confidence === "low")
36
+ return confidence;
37
+ return DEFAULT_CONFIDENCE;
38
+ }
39
+ // makeAndroidFinding (src/mobile/android/audit/androidFinding.ts) is the sole
40
+ // place Android findings compose their `evidence` string, always in the exact
41
+ // form `location=<relativePath>(:<line>)?` (optionally followed by further
42
+ // ` | `-joined detail segments). Extracting the line from that well-known,
43
+ // single-producer format is not fabricating a source position — it is
44
+ // reading a real value the same codebase already wrote, deterministically.
45
+ // Guarded so a value is only trusted when its path matches the finding's own
46
+ // affectedFiles[0]; anything else is left absent rather than guessed.
47
+ const LOCATION_EVIDENCE_PATTERN = /(?:^|\s\|\s)location=([^\s|]+)/;
48
+ function extractAndroidFindingLine(finding) {
49
+ if (!finding.evidence)
50
+ return undefined;
51
+ const relativePath = finding.affectedFiles?.[0];
52
+ if (!relativePath)
53
+ return undefined;
54
+ const match = LOCATION_EVIDENCE_PATTERN.exec(finding.evidence);
55
+ if (!match)
56
+ return undefined;
57
+ const [locationPath, lineText] = match[1].split(":");
58
+ if (locationPath !== relativePath || lineText === undefined)
59
+ return undefined;
60
+ const line = Number(lineText);
61
+ return Number.isInteger(line) && line > 0 ? line : undefined;
62
+ }
63
+ export function mapAndroidSecurityFindingToAuditIssue(finding, context) {
64
+ const severity = mapSecuritySeverityToAuditSeverity(finding.severity);
65
+ const isBlockingSeverity = isBlockingSecuritySeverity(finding.severity);
66
+ const relativePath = finding.affectedFiles?.[0];
67
+ const line = extractAndroidFindingLine(finding);
68
+ const provenanceSuffix = context.externalToolId ? `, tool=${context.externalToolId}` : "";
69
+ const evidence = finding.evidence
70
+ ? [
71
+ makeAuditEvidence({
72
+ kind: "observation",
73
+ message: `Android security validation (${context.checkId}${provenanceSuffix}): ${finding.title}`,
74
+ excerpt: finding.evidence,
75
+ filePath: relativePath,
76
+ line,
77
+ source: `android-security-validation:${context.checkId}`,
78
+ confidence: mapCheckConfidenceToAuditConfidence(context.checkConfidence),
79
+ }),
80
+ ]
81
+ : [];
82
+ const reportPath = context.reportReference?.json ?? context.reportReference?.text ?? undefined;
83
+ if (reportPath) {
84
+ evidence.push(makeAuditEvidence({
85
+ kind: "reference",
86
+ message: "Original Android security-validation report",
87
+ filePath: reportPath,
88
+ source: "android-security-validation:report",
89
+ confidence: DEFAULT_CONFIDENCE,
90
+ }));
91
+ }
92
+ return {
93
+ id: `android:${context.checkId}:${finding.id}`,
94
+ auditType: "security",
95
+ detectorId: ANDROID_SECURITY_AUDIT_DETECTOR_ID,
96
+ title: finding.title,
97
+ description: finding.description,
98
+ severity,
99
+ confidence: mapCheckConfidenceToAuditConfidence(context.checkConfidence),
100
+ falsePositiveRisk: DEFAULT_FALSE_POSITIVE_RISK,
101
+ // Deliberately the Android-specific check category (e.g.
102
+ // "android-backup-configuration"), not finding.category — every Android
103
+ // finding's own `category` is the single shared "static-scan" value, so
104
+ // reusing it here would make every mapped Android issue's category
105
+ // uselessly uniform. See module header comment.
106
+ category: context.checkCategory,
107
+ evidence,
108
+ affectedFiles: finding.affectedFiles ?? [],
109
+ recommendedAction: finding.recommendation ?? "Review the Android security validation report for remediation guidance.",
110
+ suggestedFixStrategy: finding.recommendation ?? "See the linked Android security validation report for details.",
111
+ validationCommands: ["npm run security:validate -- --profile android"],
112
+ releaseBlocking: isBlockingSeverity,
113
+ implementationBlocking: isBlockingSeverity,
114
+ autoFixEligible: false,
115
+ };
116
+ }
@@ -31,12 +31,22 @@ const SEVERITY_MAP = {
31
31
  // false positives) nor discounts it as unreliable.
32
32
  const DEFAULT_CONFIDENCE = "medium";
33
33
  const DEFAULT_FALSE_POSITIVE_RISK = "medium";
34
+ // Exported so other SecurityFinding-consuming mappers (e.g. the Android
35
+ // audit-adapter mapper, v0.4.2 Batch 1) reuse the same conservative
36
+ // severity policy instead of defining a second one.
37
+ export function mapSecuritySeverityToAuditSeverity(severity) {
38
+ return SEVERITY_MAP[severity];
39
+ }
40
+ // Mirrors verdict.ts's calculateVerdict: blocker and major findings are what
41
+ // drive a "not-ready-security-blocker-remains" verdict — minor/informational
42
+ // findings never block release or implementation on their own. Exported for
43
+ // reuse by other SecurityFinding-consuming mappers.
44
+ export function isBlockingSecuritySeverity(severity) {
45
+ return severity === "blocker" || severity === "major";
46
+ }
34
47
  export function mapSecurityFindingToAuditIssue(finding) {
35
- const severity = SEVERITY_MAP[finding.severity];
36
- // Mirrors verdict.ts's calculateVerdict: blocker and major findings are
37
- // what drive a "not-ready-security-blocker-remains" verdict — minor/
38
- // informational findings never block release or implementation on their own.
39
- const isBlockingSeverity = finding.severity === "blocker" || finding.severity === "major";
48
+ const severity = mapSecuritySeverityToAuditSeverity(finding.severity);
49
+ const isBlockingSeverity = isBlockingSecuritySeverity(finding.severity);
40
50
  const evidence = finding.evidence
41
51
  ? [
42
52
  makeAuditEvidence({
@@ -6,7 +6,9 @@ import { writeSecurityReportFiles } from "../../securityValidation/report/writeS
6
6
  import { DEFAULT_SECURITY_CHECKS, DEFAULT_SECURITY_PROFILE } from "../../securityValidation/validate/cliOptions.js";
7
7
  import { verdictToHumanLabel } from "../../securityValidation/validate/verdict.js";
8
8
  import { mapSecurityFindingToAuditIssue } from "./mapSecurityFindingToAuditIssue.js";
9
- export async function runSecurityAuditAdapter(options) {
9
+ import { runAndroidAuditIntegration } from "./androidAuditIntegration.js";
10
+ import { ANDROID_AUDIT_NOT_REQUESTED_SUMMARY, } from "./securityAuditTypes.js";
11
+ export async function runSecurityAuditAdapter(options, dependencies = {}) {
10
12
  const { toolRoot, config } = options;
11
13
  const summary = await runSecurityValidation({
12
14
  cwd: toolRoot,
@@ -60,7 +62,23 @@ export async function runSecurityAuditAdapter(options) {
60
62
  verdictReasonSummary: summary.verdictReasonSummary ?? null,
61
63
  reportPaths: { text: textPath, json: jsonPath },
62
64
  };
63
- return { issues, summary: securitySummary };
65
+ // v0.4.2 Batch 2 — Android integration is strictly additive: existing
66
+ // non-Android issues/summary above are computed identically whether or not
67
+ // Android is requested, and Android issues are always appended after them,
68
+ // never interleaved or reordered.
69
+ let androidIssues = [];
70
+ let androidSummary = ANDROID_AUDIT_NOT_REQUESTED_SUMMARY;
71
+ if (options.android?.enabled) {
72
+ const androidResult = await runAndroidAuditIntegration({
73
+ toolRoot,
74
+ targetPathArg: config.targetPathArg,
75
+ request: options.android,
76
+ runAndroidValidation: dependencies.runAndroidValidation,
77
+ });
78
+ androidIssues = androidResult.issues;
79
+ androidSummary = androidResult.summary;
80
+ }
81
+ return { issues: [...issues, ...androidIssues], summary: securitySummary, android: androidSummary };
64
82
  }
65
83
  function buildRunScopedReportSuffix(startedAt, finishedAt) {
66
84
  return `${sanitizeTimestamp(startedAt)}-${sanitizeTimestamp(finishedAt)}-${process.pid}`;
@@ -15,3 +15,40 @@ export const SECURITY_AUDIT_NOT_RUN_SUMMARY = {
15
15
  verdictReasonSummary: null,
16
16
  reportPaths: { text: null, json: null },
17
17
  };
18
+ // Mirrors SecurityAuditReportSummary's own `ran`-first, always-present
19
+ // convention: a status literal rather than inferring "did it run" from
20
+ // nullable fields, so callers/tests can switch on one value instead of
21
+ // reconstructing a state machine from several optional fields.
22
+ export const ANDROID_AUDIT_STATUSES = ["not-requested", "not-applicable", "completed", "failed"];
23
+ export const EMPTY_ANDROID_AUDIT_CANDIDATE_SUMMARY = {
24
+ totalCount: 0,
25
+ checksWithCandidates: 0,
26
+ byConfidence: { unknown: 0, low: 0, medium: 0, high: 0 },
27
+ byResolutionState: { resolved: 0, unresolved: 0, missing: 0, malformed: 0, unsupported: 0, "not-applicable": 0 },
28
+ byCategory: {},
29
+ };
30
+ export const ANDROID_AUDIT_NOT_REQUESTED_SUMMARY = {
31
+ requested: false,
32
+ applicable: null,
33
+ status: "not-requested",
34
+ verdict: null,
35
+ totalChecks: 0,
36
+ passedChecks: 0,
37
+ checksWithFindings: 0,
38
+ candidateOnlyChecks: 0,
39
+ skippedChecks: 0,
40
+ inconclusiveChecks: 0,
41
+ failedChecks: 0,
42
+ confirmedFindingCount: 0,
43
+ mappedIssueCount: 0,
44
+ candidateEvidenceCount: 0,
45
+ requestedGradleOperationCount: 0,
46
+ executedGradleOperationCount: 0,
47
+ requestedExternalToolCount: 0,
48
+ executedExternalToolCount: 0,
49
+ skippedExternalToolCount: 0,
50
+ reportPaths: { text: null, json: null },
51
+ warnings: [],
52
+ errors: [],
53
+ candidateSummary: EMPTY_ANDROID_AUDIT_CANDIDATE_SUMMARY,
54
+ };
@@ -0,0 +1,52 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalizeStageContextRun } from "./canonicalizeStageContextRun.js";
3
+ function sha256Hex(text) {
4
+ return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
5
+ }
6
+ export function calculateStageContextDeterminism(candidates) {
7
+ if (candidates.length === 0) {
8
+ throw new Error("calculateStageContextDeterminism requires at least one candidate.");
9
+ }
10
+ if (candidates.length === 1) {
11
+ return {
12
+ availability: "not-applicable",
13
+ repeatCount: 1,
14
+ deterministic: null,
15
+ baselineSha256: null,
16
+ runDigests: [],
17
+ mismatchRunNumbers: [],
18
+ reason: "Repeated-run determinism requires at least two runs."
19
+ };
20
+ }
21
+ let runDigests;
22
+ try {
23
+ runDigests = candidates.map((candidate) => ({
24
+ runNumber: candidate.runNumber,
25
+ sha256: sha256Hex(canonicalizeStageContextRun(candidate.value))
26
+ }));
27
+ }
28
+ catch {
29
+ return {
30
+ availability: "unavailable",
31
+ repeatCount: candidates.length,
32
+ deterministic: null,
33
+ baselineSha256: null,
34
+ runDigests: [],
35
+ mismatchRunNumbers: [],
36
+ reason: "Stage-context run values could not be canonicalized for determinism comparison."
37
+ };
38
+ }
39
+ const baselineSha256 = runDigests[0].sha256;
40
+ const mismatchRunNumbers = runDigests
41
+ .filter((digest) => digest.sha256 !== baselineSha256)
42
+ .map((digest) => digest.runNumber);
43
+ return {
44
+ availability: "available",
45
+ repeatCount: candidates.length,
46
+ deterministic: mismatchRunNumbers.length === 0,
47
+ baselineSha256,
48
+ runDigests,
49
+ mismatchRunNumbers,
50
+ reason: null
51
+ };
52
+ }
@@ -0,0 +1,53 @@
1
+ export function canonicalizeStageContextRun(value) {
2
+ const seen = new WeakSet();
3
+ function serialize(input) {
4
+ if (input === null)
5
+ return "null";
6
+ const kind = typeof input;
7
+ if (kind === "bigint") {
8
+ throw new TypeError("Stage-context run values must not contain BigInt.");
9
+ }
10
+ if (kind === "function") {
11
+ throw new TypeError("Stage-context run values must not contain functions.");
12
+ }
13
+ if (kind === "symbol") {
14
+ throw new TypeError("Stage-context run values must not contain symbols.");
15
+ }
16
+ if (kind === "undefined") {
17
+ return "null";
18
+ }
19
+ if (kind === "number") {
20
+ if (!Number.isFinite(input)) {
21
+ throw new RangeError("Stage-context run values must not contain non-finite numbers.");
22
+ }
23
+ return JSON.stringify(input);
24
+ }
25
+ if (kind === "boolean" || kind === "string") {
26
+ return JSON.stringify(input);
27
+ }
28
+ if (Array.isArray(input)) {
29
+ if (seen.has(input)) {
30
+ throw new TypeError("Stage-context run values must not contain circular references.");
31
+ }
32
+ seen.add(input);
33
+ const items = input.map((item) => (item === undefined ? "null" : serialize(item)));
34
+ seen.delete(input);
35
+ return `[${items.join(",")}]`;
36
+ }
37
+ if (kind === "object") {
38
+ const obj = input;
39
+ if (seen.has(obj)) {
40
+ throw new TypeError("Stage-context run values must not contain circular references.");
41
+ }
42
+ seen.add(obj);
43
+ const keys = Object.keys(obj)
44
+ .filter((key) => obj[key] !== undefined)
45
+ .sort();
46
+ const parts = keys.map((key) => `${JSON.stringify(key)}:${serialize(obj[key])}`);
47
+ seen.delete(obj);
48
+ return `{${parts.join(",")}}`;
49
+ }
50
+ throw new TypeError("Stage-context run values contain an unsupported type.");
51
+ }
52
+ return serialize(value);
53
+ }
@@ -0,0 +1,2 @@
1
+ export { canonicalizeStageContextRun } from "./canonicalizeStageContextRun.js";
2
+ export { calculateStageContextDeterminism } from "./calculateStageContextDeterminism.js";
@@ -0,0 +1,2 @@
1
+ export { validateStageContextExpectationFixtureV1 } from "./validation.js";
2
+ export { readStageContextExpectationFixtureV1 } from "./readStageContextExpectationFixtureV1.js";
@@ -0,0 +1,50 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { validateStageContextExpectationFixtureV1 } from "./validation.js";
4
+ export async function readStageContextExpectationFixtureV1(sourcePathInput) {
5
+ const sourcePath = path.resolve(sourcePathInput);
6
+ let raw;
7
+ try {
8
+ raw = await readFile(sourcePath, "utf8");
9
+ }
10
+ catch (error) {
11
+ const code = error.code;
12
+ if (code === "ENOENT") {
13
+ return {
14
+ ok: false,
15
+ sourcePath,
16
+ code: "FILE_NOT_FOUND",
17
+ message: `Stage context expectation fixture was not found at "${sourcePath}".`
18
+ };
19
+ }
20
+ return {
21
+ ok: false,
22
+ sourcePath,
23
+ code: "UNREADABLE_FILE",
24
+ message: `Stage context expectation fixture at "${sourcePath}" could not be read: ${error.message}`
25
+ };
26
+ }
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(raw);
30
+ }
31
+ catch (error) {
32
+ return {
33
+ ok: false,
34
+ sourcePath,
35
+ code: "MALFORMED_JSON",
36
+ message: `Stage context expectation fixture at "${sourcePath}" is not valid JSON: ${error.message}`
37
+ };
38
+ }
39
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
40
+ return {
41
+ ok: false,
42
+ sourcePath,
43
+ code: "NON_OBJECT_ROOT",
44
+ message: `Stage context expectation fixture at "${sourcePath}" must parse to a JSON object at the root.`,
45
+ expected: "object",
46
+ actual: parsed
47
+ };
48
+ }
49
+ return validateStageContextExpectationFixtureV1(parsed, sourcePath);
50
+ }