@burdenoff/microfe-atheletebridge 2026.1024.1 → 2026.1031.1

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 (27) hide show
  1. package/dist/atheletebridge/components/ai/DecisionCard.d.ts.map +1 -1
  2. package/dist/atheletebridge/components/ai/DecisionCard.js +217 -122
  3. package/dist/atheletebridge/components/ai/DecisionCard.js.map +1 -1
  4. package/dist/atheletebridge/components/ai/SubjectDecisions.d.ts +29 -0
  5. package/dist/atheletebridge/components/ai/SubjectDecisions.d.ts.map +1 -0
  6. package/dist/atheletebridge/components/ai/SubjectDecisions.js +69 -0
  7. package/dist/atheletebridge/components/ai/SubjectDecisions.js.map +1 -0
  8. package/dist/atheletebridge/components/ai/index.d.ts +2 -0
  9. package/dist/atheletebridge/components/ai/index.d.ts.map +1 -1
  10. package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +10 -1
  11. package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
  12. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +24 -15
  13. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
  14. package/dist/atheletebridge/pages/athletes/AthleteProfilePage.d.ts.map +1 -1
  15. package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js +135 -118
  16. package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -1
  17. package/dist/atheletebridge/pages/programs/ProgramDetailPage.d.ts.map +1 -1
  18. package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +42 -34
  19. package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
  20. package/dist/atheletebridge/pages/teams/TeamDetailPage.d.ts.map +1 -1
  21. package/dist/atheletebridge/pages/teams/TeamDetailPage.js +65 -57
  22. package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
  23. package/dist/atheletebridge/utils/aiDecision.d.ts +12 -0
  24. package/dist/atheletebridge/utils/aiDecision.d.ts.map +1 -1
  25. package/dist/atheletebridge/utils/aiDecision.js +11 -1
  26. package/dist/atheletebridge/utils/aiDecision.js.map +1 -1
  27. package/package.json +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"aiDecision.js","names":[],"sources":["../../../src/atheletebridge/utils/aiDecision.ts"],"sourcesContent":["/**\n * AI ledger payload readers\n * @module atheletebridge/utils/aiDecision\n *\n * `AthAgentRun.decision` and `AthAgentRun.inputRefs` are JSON by contract: the\n * decision engine writes a different shape per decision path (a performance\n * assessment, a coach override, a generated program plan) and, when the model's\n * reply could not be parsed, a `{ parseError }` marker on an otherwise\n * SUCCEEDED row.\n *\n * Two rules govern this file:\n *\n * 1. NOTHING IS HIDDEN. A payload shape this module does not recognise is still\n * rendered, key by key, via `extra`. A card that silently drops unknown keys\n * is how a failed decision reads as an empty success.\n * 2. NOTHING IS CAST. Every read is a guarded lookup on `unknown`, so a payload\n * that disagrees with the shape we expect degrades to \"shown generically\"\n * rather than to a runtime crash inside the render.\n */\n\nimport type { AthAiAssessment } from '../types';\n\n/** One rendered key/value pair from a JSON payload. */\nexport interface AiPayloadEntry {\n key: string;\n label: string;\n value: string;\n}\n\nexport type AiDecisionKind =\n /** No decision recorded (a FAILED run never reached one). */\n | 'none'\n /** The provider replied but the reply was unusable — `{ parseError }`. */\n | 'parse-failure'\n /** `athAdjustPlanFromPerformance` — the AI's read of an athlete's trend. */\n | 'assessment'\n /** `athOverrideAgentRun` — a coach's review of a prior decision. */\n | 'override'\n /** `athGenerateProgram` — a generated multi-week plan. */\n | 'program-plan'\n /** Recognisably JSON, but not a shape this build knows. */\n | 'other';\n\nexport interface AiDecisionSummary {\n kind: AiDecisionKind;\n /** Short headline for the decision card. */\n title: string;\n /** True when the card must read as a failure, not as an empty success. */\n isFailure: boolean;\n assessment: AthAiAssessment | null;\n /** Resolution written by the engine: PROPOSED / ACCEPTED / OVERRIDDEN. */\n status: string | null;\n rationale: string | null;\n note: string | null;\n confidence: number | null;\n recommendedWeeklyLoadChangePct: number | null;\n flags: string[];\n parseError: string | null;\n /** Every payload key not surfaced in a dedicated field above. */\n extra: AiPayloadEntry[];\n}\n\nconst ASSESSMENT_VALUES: readonly AthAiAssessment[] = [\n 'AT_RISK',\n 'DELOAD',\n 'MAINTAIN',\n 'PROGRESSION',\n];\n\n/** Keys rendered through a dedicated field on {@link AiDecisionSummary}. */\nconst DEDICATED_DECISION_KEYS = new Set([\n 'assessment',\n 'status',\n 'rationale',\n 'note',\n 'confidence',\n 'recommendedWeeklyLoadChangePct',\n 'flags',\n 'parseError',\n]);\n\n/** Human labels for the provenance keys the decision engine actually writes. */\nconst PAYLOAD_LABELS: Record<string, string> = {\n agentRunId: 'Agent run',\n athleteId: 'Athlete',\n avgRpe: 'Average RPE',\n avgTotalLoad: 'Average total load',\n constraints: 'Constraints',\n decidedAt: 'Decided at',\n decidedBy: 'Decided by',\n goals: 'Goals',\n level: 'Level',\n loadTrend: 'Load trend',\n metricCount: 'Metrics considered',\n metricSummaries: 'Metric summaries',\n programId: 'Program',\n proposedAssessment: 'AI proposed',\n requestedWeeks: 'Requested weeks',\n rpeTrend: 'RPE trend',\n source: 'Source',\n sport: 'Sport',\n statusCounts: 'Workout status counts',\n supersedesRunId: 'Supersedes run',\n teamId: 'Team',\n weeks: 'Weeks',\n workoutLogCount: 'Workout logs considered',\n};\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Title-cased label for a payload key (`avgRpe` → `Avg rpe`). */\nexport function humanizePayloadKey(key: string): string {\n const known = PAYLOAD_LABELS[key];\n if (known) {\n return known;\n }\n const spaced = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[_-]+/g, ' ')\n .trim()\n .toLowerCase();\n return spaced ? spaced.charAt(0).toUpperCase() + spaced.slice(1) : key;\n}\n\n/** Render any JSON value as a single readable line. */\nexport function formatPayloadValue(value: unknown): string {\n if (value === null || value === undefined) {\n return '—';\n }\n if (typeof value === 'string') {\n return value.trim() ? value : '—';\n }\n if (typeof value === 'number') {\n return Number.isFinite(value) ? String(value) : '—';\n }\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (Array.isArray(value)) {\n return value.length === 0 ? '—' : value.map((item) => formatPayloadValue(item)).join(', ');\n }\n if (isRecord(value)) {\n const parts = Object.entries(value).map(\n ([key, nested]) => `${humanizePayloadKey(key)}: ${formatPayloadValue(nested)}`\n );\n return parts.length === 0 ? '—' : parts.join(' · ');\n }\n return String(value);\n}\n\n/** Flatten a JSON payload into ordered, renderable entries. Never drops a key. */\nexport function toPayloadEntries(\n payload: unknown,\n skipKeys?: ReadonlySet<string>\n): AiPayloadEntry[] {\n if (!isRecord(payload)) {\n return [];\n }\n return Object.entries(payload)\n .filter(([key]) => !skipKeys?.has(key))\n .map(([key, value]) => ({\n key,\n label: humanizePayloadKey(key),\n value: formatPayloadValue(value),\n }));\n}\n\n/**\n * Provenance entries for a run's `inputRefs` — the ids and aggregate stats the\n * model actually considered. Keys whose value is null/undefined are dropped\n * (the engine writes `{ teamId, athleteId }` with one of them unset).\n */\nexport function toProvenanceEntries(inputRefs: unknown): AiPayloadEntry[] {\n if (!isRecord(inputRefs)) {\n return [];\n }\n return Object.entries(inputRefs)\n .filter(([, value]) => value !== null && value !== undefined)\n .map(([key, value]) => ({\n key,\n label: humanizePayloadKey(key),\n value: formatPayloadValue(value),\n }));\n}\n\nfunction readString(record: Record<string, unknown>, key: string): string | null {\n const value = record[key];\n return typeof value === 'string' && value.trim() ? value : null;\n}\n\nfunction readNumber(record: Record<string, unknown>, key: string): number | null {\n const value = record[key];\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\nfunction readAssessment(record: Record<string, unknown>): AthAiAssessment | null {\n const value = record.assessment;\n return typeof value === 'string' && (ASSESSMENT_VALUES as readonly string[]).includes(value)\n ? (value as AthAiAssessment)\n : null;\n}\n\nfunction readFlags(record: Record<string, unknown>): string[] {\n const value = record.flags;\n if (!Array.isArray(value)) {\n return [];\n }\n return value.filter((flag): flag is string => typeof flag === 'string' && flag.trim().length > 0);\n}\n\nconst EMPTY_SUMMARY: Omit<AiDecisionSummary, 'kind' | 'title' | 'isFailure'> = {\n assessment: null,\n status: null,\n rationale: null,\n note: null,\n confidence: null,\n recommendedWeeklyLoadChangePct: null,\n flags: [],\n parseError: null,\n extra: [],\n};\n\n/**\n * Classify and unpack a ledger row's `decision` payload for rendering.\n *\n * A `parseError` payload is classified as a FAILURE even though the ledger row\n * itself is SUCCEEDED (the provider answered; the answer was unusable) — that\n * distinction is the whole point of recording it, so the card must show it.\n */\nexport function summarizeDecision(decision: unknown): AiDecisionSummary {\n if (decision === null || decision === undefined) {\n return { kind: 'none', title: 'No decision recorded', isFailure: false, ...EMPTY_SUMMARY };\n }\n\n if (!isRecord(decision)) {\n return {\n kind: 'other',\n title: 'Decision payload',\n isFailure: false,\n ...EMPTY_SUMMARY,\n extra: [{ key: 'value', label: 'Value', value: formatPayloadValue(decision) }],\n };\n }\n\n const parseError = readString(decision, 'parseError');\n if (parseError) {\n return {\n kind: 'parse-failure',\n title: 'Unusable model reply',\n isFailure: true,\n ...EMPTY_SUMMARY,\n parseError,\n extra: toPayloadEntries(decision, DEDICATED_DECISION_KEYS),\n };\n }\n\n const assessment = readAssessment(decision);\n const note = readString(decision, 'note');\n const status = readString(decision, 'status');\n const rationale = readString(decision, 'rationale');\n const extra = toPayloadEntries(decision, DEDICATED_DECISION_KEYS);\n\n if (assessment && note) {\n // A coach override: assessment + note + resolution status, no rationale.\n return {\n ...EMPTY_SUMMARY,\n kind: 'override',\n title: 'Coach decision',\n isFailure: false,\n assessment,\n status,\n note,\n rationale,\n flags: readFlags(decision),\n extra,\n };\n }\n\n if (assessment) {\n return {\n ...EMPTY_SUMMARY,\n kind: 'assessment',\n title: 'Performance assessment',\n isFailure: false,\n assessment,\n status,\n rationale,\n confidence: readNumber(decision, 'confidence'),\n recommendedWeeklyLoadChangePct: readNumber(decision, 'recommendedWeeklyLoadChangePct'),\n flags: readFlags(decision),\n extra,\n };\n }\n\n if (Array.isArray(decision.weeks)) {\n return {\n ...EMPTY_SUMMARY,\n kind: 'program-plan',\n title: 'Generated training plan',\n isFailure: false,\n rationale,\n extra,\n };\n }\n\n return {\n ...EMPTY_SUMMARY,\n kind: 'other',\n title: 'Decision payload',\n isFailure: false,\n status,\n rationale,\n note,\n flags: readFlags(decision),\n extra,\n };\n}\n\n/** Format a confidence in 0–1 as a percentage, or null when absent. */\nexport function formatConfidence(confidence: number | null): string | null {\n if (confidence === null) {\n return null;\n }\n const clamped = Math.min(Math.max(confidence, 0), 1);\n return `${Math.round(clamped * 100)}%`;\n}\n\n/** Format a signed weekly-load change, e.g. `-10%` / `+5%`. */\nexport function formatLoadChange(pct: number | null): string | null {\n if (pct === null) {\n return null;\n }\n const rounded = Math.round(pct * 10) / 10;\n return `${rounded > 0 ? '+' : ''}${rounded}%`;\n}\n\n/**\n * The program an override can legally be filed against, read off the run's\n * provenance. `athAdjustPlanFromPerformance` and earlier overrides name it in\n * `inputRefs.programId`; `athGenerateProgram` runs BEFORE the program exists,\n * so its runs carry no program id and the coach must choose one (the service\n * validates the link and answers NOT_FOUND when the run never touched it).\n */\nexport function programIdFromInputRefs(inputRefs: unknown): string | null {\n if (!isRecord(inputRefs)) {\n return null;\n }\n return readString(inputRefs, 'programId');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"aiDecision.js","names":[],"sources":["../../../src/atheletebridge/utils/aiDecision.ts"],"sourcesContent":["/**\n * AI ledger payload readers\n * @module atheletebridge/utils/aiDecision\n *\n * `AthAgentRun.decision` and `AthAgentRun.inputRefs` are JSON by contract: the\n * decision engine writes a different shape per decision path (a performance\n * assessment, a coach override, a generated program plan) and, when the model's\n * reply could not be parsed, a `{ parseError }` marker on an otherwise\n * SUCCEEDED row.\n *\n * Two rules govern this file:\n *\n * 1. NOTHING IS HIDDEN. A payload shape this module does not recognise is still\n * rendered, key by key, via `extra`. A card that silently drops unknown keys\n * is how a failed decision reads as an empty success.\n * 2. NOTHING IS CAST. Every read is a guarded lookup on `unknown`, so a payload\n * that disagrees with the shape we expect degrades to \"shown generically\"\n * rather than to a runtime crash inside the render.\n */\n\nimport type { AthAiAssessment } from '../types';\n\n/** One rendered key/value pair from a JSON payload. */\nexport interface AiPayloadEntry {\n key: string;\n label: string;\n value: string;\n}\n\nexport type AiDecisionKind =\n /** No decision recorded (a FAILED run never reached one). */\n | 'none'\n /** The provider replied but the reply was unusable — `{ parseError }`. */\n | 'parse-failure'\n /** `athAdjustPlanFromPerformance` — the AI's read of an athlete's trend. */\n | 'assessment'\n /** `athOverrideAgentRun` — a coach's review of a prior decision. */\n | 'override'\n /** `athGenerateProgram` — a generated multi-week plan. */\n | 'program-plan'\n /** Recognisably JSON, but not a shape this build knows. */\n | 'other';\n\nexport interface AiDecisionSummary {\n kind: AiDecisionKind;\n /** Short headline for the decision card. */\n title: string;\n /** True when the card must read as a failure, not as an empty success. */\n isFailure: boolean;\n assessment: AthAiAssessment | null;\n /** Resolution written by the engine: PROPOSED / ACCEPTED / OVERRIDDEN. */\n status: string | null;\n rationale: string | null;\n note: string | null;\n confidence: number | null;\n recommendedWeeklyLoadChangePct: number | null;\n flags: string[];\n parseError: string | null;\n /** Every payload key not surfaced in a dedicated field above. */\n extra: AiPayloadEntry[];\n}\n\nconst ASSESSMENT_VALUES: readonly AthAiAssessment[] = [\n 'AT_RISK',\n 'DELOAD',\n 'MAINTAIN',\n 'PROGRESSION',\n];\n\n/** Keys rendered through a dedicated field on {@link AiDecisionSummary}. */\nconst DEDICATED_DECISION_KEYS = new Set([\n 'assessment',\n 'status',\n 'rationale',\n 'note',\n 'confidence',\n 'recommendedWeeklyLoadChangePct',\n 'flags',\n 'parseError',\n]);\n\n/** Human labels for the provenance keys the decision engine actually writes. */\nconst PAYLOAD_LABELS: Record<string, string> = {\n agentRunId: 'Agent run',\n athleteId: 'Athlete',\n avgRpe: 'Average RPE',\n avgTotalLoad: 'Average total load',\n constraints: 'Constraints',\n decidedAt: 'Decided at',\n decidedBy: 'Decided by',\n goals: 'Goals',\n level: 'Level',\n loadTrend: 'Load trend',\n metricCount: 'Metrics considered',\n metricSummaries: 'Metric summaries',\n programId: 'Program',\n proposedAssessment: 'AI proposed',\n requestedWeeks: 'Requested weeks',\n rpeTrend: 'RPE trend',\n source: 'Source',\n sport: 'Sport',\n statusCounts: 'Workout status counts',\n supersedesRunId: 'Supersedes run',\n teamId: 'Team',\n weeks: 'Weeks',\n workoutLogCount: 'Workout logs considered',\n};\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Title-cased label for a payload key (`avgRpe` → `Avg rpe`). */\nexport function humanizePayloadKey(key: string): string {\n const known = PAYLOAD_LABELS[key];\n if (known) {\n return known;\n }\n const spaced = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[_-]+/g, ' ')\n .trim()\n .toLowerCase();\n return spaced ? spaced.charAt(0).toUpperCase() + spaced.slice(1) : key;\n}\n\n/** Render any JSON value as a single readable line. */\nexport function formatPayloadValue(value: unknown): string {\n if (value === null || value === undefined) {\n return '—';\n }\n if (typeof value === 'string') {\n return value.trim() ? value : '—';\n }\n if (typeof value === 'number') {\n return Number.isFinite(value) ? String(value) : '—';\n }\n if (typeof value === 'boolean') {\n return value ? 'Yes' : 'No';\n }\n if (Array.isArray(value)) {\n return value.length === 0 ? '—' : value.map((item) => formatPayloadValue(item)).join(', ');\n }\n if (isRecord(value)) {\n const parts = Object.entries(value).map(\n ([key, nested]) => `${humanizePayloadKey(key)}: ${formatPayloadValue(nested)}`\n );\n return parts.length === 0 ? '—' : parts.join(' · ');\n }\n return String(value);\n}\n\n/** Flatten a JSON payload into ordered, renderable entries. Never drops a key. */\nexport function toPayloadEntries(\n payload: unknown,\n skipKeys?: ReadonlySet<string>\n): AiPayloadEntry[] {\n if (!isRecord(payload)) {\n return [];\n }\n return Object.entries(payload)\n .filter(([key]) => !skipKeys?.has(key))\n .map(([key, value]) => ({\n key,\n label: humanizePayloadKey(key),\n value: formatPayloadValue(value),\n }));\n}\n\n/**\n * Provenance entries for a run's `inputRefs` — the ids and aggregate stats the\n * model actually considered. Keys whose value is null/undefined are dropped\n * (the engine writes `{ teamId, athleteId }` with one of them unset).\n */\nexport function toProvenanceEntries(inputRefs: unknown): AiPayloadEntry[] {\n if (!isRecord(inputRefs)) {\n return [];\n }\n return Object.entries(inputRefs)\n .filter(([, value]) => value !== null && value !== undefined)\n .map(([key, value]) => ({\n key,\n label: humanizePayloadKey(key),\n value: formatPayloadValue(value),\n }));\n}\n\nfunction readString(record: Record<string, unknown>, key: string): string | null {\n const value = record[key];\n return typeof value === 'string' && value.trim() ? value : null;\n}\n\nfunction readNumber(record: Record<string, unknown>, key: string): number | null {\n const value = record[key];\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\nfunction readAssessment(record: Record<string, unknown>): AthAiAssessment | null {\n const value = record.assessment;\n return typeof value === 'string' && (ASSESSMENT_VALUES as readonly string[]).includes(value)\n ? (value as AthAiAssessment)\n : null;\n}\n\nfunction readFlags(record: Record<string, unknown>): string[] {\n const value = record.flags;\n if (!Array.isArray(value)) {\n return [];\n }\n return value.filter((flag): flag is string => typeof flag === 'string' && flag.trim().length > 0);\n}\n\nconst EMPTY_SUMMARY: Omit<AiDecisionSummary, 'kind' | 'title' | 'isFailure'> = {\n assessment: null,\n status: null,\n rationale: null,\n note: null,\n confidence: null,\n recommendedWeeklyLoadChangePct: null,\n flags: [],\n parseError: null,\n extra: [],\n};\n\n/**\n * Classify and unpack a ledger row's `decision` payload for rendering.\n *\n * A `parseError` payload is classified as a FAILURE even though the ledger row\n * itself is SUCCEEDED (the provider answered; the answer was unusable) — that\n * distinction is the whole point of recording it, so the card must show it.\n */\nexport function summarizeDecision(decision: unknown): AiDecisionSummary {\n if (decision === null || decision === undefined) {\n return { kind: 'none', title: 'No decision recorded', isFailure: false, ...EMPTY_SUMMARY };\n }\n\n if (!isRecord(decision)) {\n return {\n kind: 'other',\n title: 'Decision payload',\n isFailure: false,\n ...EMPTY_SUMMARY,\n extra: [{ key: 'value', label: 'Value', value: formatPayloadValue(decision) }],\n };\n }\n\n const parseError = readString(decision, 'parseError');\n if (parseError) {\n return {\n kind: 'parse-failure',\n title: 'Unusable model reply',\n isFailure: true,\n ...EMPTY_SUMMARY,\n parseError,\n extra: toPayloadEntries(decision, DEDICATED_DECISION_KEYS),\n };\n }\n\n const assessment = readAssessment(decision);\n const note = readString(decision, 'note');\n const status = readString(decision, 'status');\n const rationale = readString(decision, 'rationale');\n const extra = toPayloadEntries(decision, DEDICATED_DECISION_KEYS);\n\n if (assessment && note) {\n // A coach override: assessment + note + resolution status, no rationale.\n return {\n ...EMPTY_SUMMARY,\n kind: 'override',\n title: 'Coach decision',\n isFailure: false,\n assessment,\n status,\n note,\n rationale,\n flags: readFlags(decision),\n extra,\n };\n }\n\n if (assessment) {\n return {\n ...EMPTY_SUMMARY,\n kind: 'assessment',\n title: 'Performance assessment',\n isFailure: false,\n assessment,\n status,\n rationale,\n confidence: readNumber(decision, 'confidence'),\n recommendedWeeklyLoadChangePct: readNumber(decision, 'recommendedWeeklyLoadChangePct'),\n flags: readFlags(decision),\n extra,\n };\n }\n\n if (Array.isArray(decision.weeks)) {\n return {\n ...EMPTY_SUMMARY,\n kind: 'program-plan',\n title: 'Generated training plan',\n isFailure: false,\n rationale,\n extra,\n };\n }\n\n return {\n ...EMPTY_SUMMARY,\n kind: 'other',\n title: 'Decision payload',\n isFailure: false,\n status,\n rationale,\n note,\n flags: readFlags(decision),\n extra,\n };\n}\n\n/** Format a confidence in 0–1 as a percentage, or null when absent. */\nexport function formatConfidence(confidence: number | null): string | null {\n if (confidence === null) {\n return null;\n }\n const clamped = Math.min(Math.max(confidence, 0), 1);\n return `${Math.round(clamped * 100)}%`;\n}\n\n/** Format a signed weekly-load change, e.g. `-10%` / `+5%`. */\nexport function formatLoadChange(pct: number | null): string | null {\n if (pct === null) {\n return null;\n }\n const rounded = Math.round(pct * 10) / 10;\n return `${rounded > 0 ? '+' : ''}${rounded}%`;\n}\n\n/**\n * The program an override can legally be filed against, read off the run's\n * provenance. `athAdjustPlanFromPerformance` and earlier overrides name it in\n * `inputRefs.programId`; `athGenerateProgram` runs BEFORE the program exists,\n * so its runs carry no program id and the coach must choose one (the service\n * validates the link and answers NOT_FOUND when the run never touched it).\n */\nexport function programIdFromInputRefs(inputRefs: unknown): string | null {\n if (!isRecord(inputRefs)) {\n return null;\n }\n return readString(inputRefs, 'programId');\n}\n\n/**\n * Where an AI proposal stands, read from its program's `context.lastAiAdjustment`.\n *\n * - `pending` — the program's current read IS this run and no coach has\n * resolved it yet (status PROPOSED). Accept/override applies.\n * - `accepted` /\n * `overridden` — a coach resolved THIS run (`overridesRunId` names it).\n * - `superseded` — the program has moved on to a different, later read.\n * - `unknown` — the program carries no AI read at all (or is unreadable).\n */\nexport type ProposalState = 'pending' | 'accepted' | 'overridden' | 'superseded' | 'unknown';\n\nexport function proposalStateFor(runId: string, programContext: unknown): ProposalState {\n const adjustment = isRecord(programContext) ? programContext.lastAiAdjustment : null;\n if (!isRecord(adjustment)) return 'unknown';\n const status = typeof adjustment.status === 'string' ? adjustment.status : null;\n if (adjustment.overridesRunId === runId) {\n if (status === 'ACCEPTED') return 'accepted';\n if (status === 'OVERRIDDEN') return 'overridden';\n }\n if (adjustment.agentRunId === runId) {\n return status === 'PROPOSED' ? 'pending' : 'unknown';\n }\n return 'superseded';\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-atheletebridge",
3
- "version": "2026.1024.1",
3
+ "version": "2026.1031.1",
4
4
  "description": "Burdenoff AthleteBridge Microfrontend - Sports, coaching & training platform for teams, fixtures, programs, sessions and athlete performance",
5
5
  "type": "module",
6
6
  "files": [
@@ -105,18 +105,19 @@
105
105
  "@burdenoff/fe-libs": "2026.801.1",
106
106
  "lodash": ">=4.17.21",
107
107
  "lodash-es": ">=4.17.21",
108
- "dompurify": ">=3.3.4",
108
+ "dompurify": ">=3.4.17",
109
109
  "fast-xml-parser": ">=4.5.5",
110
110
  "@xmldom/xmldom": ">=0.8.12",
111
111
  "swiper": ">=12.1.2",
112
112
  "brace-expansion": ">=1.1.13",
113
- "follow-redirects": ">=1.15.12",
113
+ "follow-redirects": ">=1.16.1",
114
114
  "smol-toml": ">=1.6.1",
115
115
  "axios": ">=1.15.0",
116
116
  "flatted": ">=3.4.1",
117
117
  "yaml": ">=2.8.3",
118
118
  "picomatch": ">=2.3.2",
119
119
  "immutable": ">=3.8.3",
120
- "minimatch": ">=3.1.3"
120
+ "minimatch": ">=3.1.3",
121
+ "zod": ">=4.6.6"
121
122
  }
122
123
  }