Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions packages/mcp/src/lib/container-status.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
import { describe, expect, it } from 'vitest';
import { resolveContainerStatus } from './container-status.js';

describe('resolveContainerStatus', () => {
it('uses the API current_status verbatim as the headline status', () => {
const result = resolveContainerStatus({
current_status: 'available',
pod_arrived_at: '2026-01-01T00:00:00Z',
pod_discharged_at: '2026-01-02T00:00:00Z',
available_for_pickup: true,
});

expect(result.status).toBe('available');
expect(result.status_source).toBe('current_status');
});

it('falls back to a derived lifecycle label only when current_status is absent', () => {
const result = resolveContainerStatus({
pod_arrived_at: null,
pod_discharged_at: null,
});

// No API status to surface, so the headline is the derived label.
expect(result.status).toBe('in_transit');
expect(result.status_source).toBe('derived');
});

it('never reports a delivered lifecycle when delivered_at is null', () => {
// pod_full_out_at is set but the API has NOT confirmed delivery.
const result = resolveContainerStatus({
current_status: 'picked_up',
pod_arrived_at: '2026-01-01T00:00:00Z',
pod_discharged_at: '2026-01-02T00:00:00Z',
pod_full_out_at: '2026-01-06T00:00:00Z',
delivered_at: null,
});

expect(result.status).toBe('picked_up');
expect(result.derived_lifecycle).not.toBe('delivered');
});

it('reports a delivered lifecycle only when delivered_at is present', () => {
const result = resolveContainerStatus({
current_status: 'delivered',
pod_arrived_at: '2026-01-01T00:00:00Z',
pod_discharged_at: '2026-01-02T00:00:00Z',
delivered_at: '2026-01-07T00:00:00Z',
});

expect(result.derived_lifecycle).toBe('delivered');
});

it('marks the derived lifecycle as non-authoritative steering metadata', () => {
const result = resolveContainerStatus({
current_status: 'on_rail',
pod_arrived_at: '2026-01-01T00:00:00Z',
pod_discharged_at: '2026-01-02T00:00:00Z',
pod_rail_loaded_at: '2026-01-03T00:00:00Z',
});

expect(result.lifecycle_is_authoritative).toBe(false);
expect(result.derived_lifecycle).toBe('on_rail');
});

it('derives discharged vs available_for_pickup from availability', () => {
const discharged = resolveContainerStatus({
pod_arrived_at: '2026-01-01T00:00:00Z',
pod_discharged_at: '2026-01-02T00:00:00Z',
available_for_pickup: false,
});
expect(discharged.derived_lifecycle).toBe('discharged');

const available = resolveContainerStatus({
pod_arrived_at: '2026-01-01T00:00:00Z',
pod_discharged_at: '2026-01-02T00:00:00Z',
available_for_pickup: true,
});
expect(available.derived_lifecycle).toBe('available_for_pickup');
});

it('handles a completely empty attribute object without throwing', () => {
const result = resolveContainerStatus({});
expect(result.status).toBe('in_transit');
expect(result.status_source).toBe('derived');
});
});
82 changes: 82 additions & 0 deletions packages/mcp/src/lib/container-status.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
/**
* Container status resolution.
*
* The Terminal49 API ships an authoritative `current_status` enum on every
* container. That value is the single source of truth for the headline status
* and MUST be surfaced verbatim — earlier code invented its own vocabulary,
* producing a three-way divergence (API status vs. derived lifecycle vs. search
* status). Here the API status is the headline; the derived lifecycle is kept
* only as clearly-labeled, non-authoritative steering metadata to help select
* follow-up tooling.
*
* The one hard rule on the derived lifecycle: never label a container
* "delivered" unless the API has actually set `delivered_at`.
*/

export type DerivedLifecycle =
| 'in_transit'
| 'arrived'
| 'discharged'
| 'available_for_pickup'
| 'at_terminal'
| 'on_rail'
| 'delivered';

export interface ContainerStatusResult {
/** Headline status shown to the user. Prefers the API `current_status`. */
status: string;
/** Where the headline came from: the API field or our derived fallback. */
status_source: 'current_status' | 'derived';
/** Heuristic lifecycle stage — steering metadata only, NOT authoritative. */
derived_lifecycle: DerivedLifecycle;
/** Always false: the derived lifecycle must never be treated as truth. */
lifecycle_is_authoritative: false;
}

interface ContainerStatusAttrs {
current_status?: string | null;
delivered_at?: string | null;
pod_arrived_at?: string | null;
pod_discharged_at?: string | null;
pod_rail_loaded_at?: string | null;
pod_full_out_at?: string | null;
final_destination_full_out_at?: string | null;
available_for_pickup?: boolean | null;
}

/**
* Compute a derived lifecycle stage from raw milestone timestamps. Used only as
* steering metadata. Delivery is gated strictly on `delivered_at` being set.
*/
function deriveLifecycle(attrs: ContainerStatusAttrs): DerivedLifecycle {
if (!attrs.pod_arrived_at) return 'in_transit';
if (!attrs.pod_discharged_at) return 'arrived';

// Only the API's explicit delivery confirmation may yield "delivered".
if (attrs.delivered_at) return 'delivered';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive delivery from a real API field

For delivered containers this branch never fires because the public container attributes schema/generated types do not define delivered_at (the actual payload exposes current_status: "delivered" and full-out timestamps). That means _metadata.derived_lifecycle and presentation_guidance fall through to discharged/at_terminal, so the tool can steer the LLM toward pickup/demurrage actions instead of confirming delivery. Use current_status === 'delivered' or a real container milestone field for this gate.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging for a maintainer decision rather than auto-resolving — this is a legitimate open design question, not something already fixed.

Confirmed details:

  • delivered_at is real in the API payload (present in the container fixtures: containers.get.base.json, containers.get.include.json, containers.list.json) but is NOT yet in the generated OpenAPI container-attributes schema (current_status and import_deadlines are the only attributes typed in that region of generated/terminal49.ts). So the comment is right that the typed schema does not define it, but it is a field the API does emit.
  • The headline status is unaffected: resolveContainerStatus surfaces current_status verbatim, so a current_status: "delivered" container correctly reads "delivered" to the user. The delivered_at gate only governs the explicitly non-authoritative derived_lifecycle and the presentation_guidance/suggestions steering keyed off it.
  • The strict delivered_at-only gate is a deliberate, documented design rule in container-status.ts ("never label a container delivered unless the API has actually set delivered_at"). The fixtures also show current_status: "picked_up" with delivered_at: null and pod_full_out_at set, i.e. terminal-complete containers where the derived lifecycle would fall through to at_terminal/discharged and the guidance could lean toward pickup/demurrage.

So the trade-off is real: switching the gate to current_status === "delivered" (or a full-out milestone) would make the steering metadata track delivery, but it changes the deliberate conservative derivation and would need test updates (container-status.test.ts asserts the current delivered_at behavior). Not resolving — please confirm the intended steering behavior for delivered/picked_up containers and whether to broaden the gate.


if (attrs.pod_rail_loaded_at && !attrs.final_destination_full_out_at) {
return 'on_rail';
}

if (attrs.available_for_pickup === true) return 'available_for_pickup';
if (attrs.available_for_pickup === false) return 'discharged';
return 'at_terminal';
}

export function resolveContainerStatus(
attrs: ContainerStatusAttrs,
): ContainerStatusResult {
const derived = deriveLifecycle(attrs);
const apiStatus =
typeof attrs.current_status === 'string' && attrs.current_status.length > 0
? attrs.current_status
: null;

return {
status: apiStatus ?? derived,
status_source: apiStatus ? 'current_status' : 'derived',
derived_lifecycle: derived,
lifecycle_is_authoritative: false,
};
}
156 changes: 156 additions & 0 deletions packages/mcp/src/lib/demurrage.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
import { describe, expect, it } from 'vitest';
import { evaluateDemurrageUrgency } from './demurrage.js';

const NOW = new Date('2026-02-10T00:00:00Z');

describe('evaluateDemurrageUrgency', () => {
it('surfaces real fees with currency and never fabricates a per-day estimate', () => {
const result = evaluateDemurrageUrgency(
{
fees_at_pod_terminal: [
{ type: 'demurrage', amount: 320, currency_code: 'USD' },
{ type: 'exam', amount: 75, currency_code: 'USD' },
],
terminal_checked_at: '2026-02-09T00:00:00Z',
},
NOW,
);

expect(result.fees).toEqual([
{ type: 'demurrage', amount: 320, currency_code: 'USD' },
{ type: 'exam', amount: 75, currency_code: 'USD' },
]);
expect(result.total_amount).toBe(395);
expect(result.currency_code).toBe('USD');
// No invented "~$75-150/day" guidance anywhere in the output.
expect(JSON.stringify(result)).not.toMatch(/\/day/);
expect(JSON.stringify(result)).not.toMatch(/75-150/);
});

it('flags overdue urgency when LFD is in the past and data is fresh', () => {
const result = evaluateDemurrageUrgency(
{
pickup_lfd: '2026-02-05T00:00:00Z',
terminal_checked_at: '2026-02-09T00:00:00Z',
},
NOW,
);

expect(result.urgency).toBe('overdue');
expect(result.days_until_lfd).toBe(-5);
expect(result.urgency_suppressed).toBe(false);
});

it('flags imminent urgency when LFD is within 3 days and data is fresh', () => {
const result = evaluateDemurrageUrgency(
{
pickup_lfd: '2026-02-12T00:00:00Z',
terminal_checked_at: '2026-02-09T00:00:00Z',
},
NOW,
);

expect(result.urgency).toBe('imminent');
expect(result.days_until_lfd).toBe(2);
});

it('suppresses urgency when terminal data is stale', () => {
const result = evaluateDemurrageUrgency(
{
pickup_lfd: '2026-02-05T00:00:00Z',
// Last checked 20 days before now -> stale (default threshold 7d).
terminal_checked_at: '2026-01-21T00:00:00Z',
},
NOW,
);

expect(result.urgency).toBe('unknown');
expect(result.urgency_suppressed).toBe(true);
expect(result.suppression_reason).toContain('stale');
});

it('suppresses urgency when tracking is stopped/closed', () => {
const result = evaluateDemurrageUrgency(
{
pickup_lfd: '2026-02-05T00:00:00Z',
terminal_checked_at: '2026-02-09T00:00:00Z',
tracking_stopped: true,
},
NOW,
);

expect(result.urgency).toBe('unknown');
expect(result.urgency_suppressed).toBe(true);
expect(result.suppression_reason).toContain('tracking');
});

it('suppresses urgency when terminal was never checked', () => {
const result = evaluateDemurrageUrgency(
{
pickup_lfd: '2026-02-05T00:00:00Z',
terminal_checked_at: null,
},
NOW,
);

expect(result.urgency).toBe('unknown');
expect(result.urgency_suppressed).toBe(true);
});

it('reports no urgency (none) when LFD is comfortably in the future', () => {
const result = evaluateDemurrageUrgency(
{
pickup_lfd: '2026-03-01T00:00:00Z',
terminal_checked_at: '2026-02-09T00:00:00Z',
},
NOW,
);

expect(result.urgency).toBe('none');
expect(result.urgency_suppressed).toBe(false);
});

it('classifies urgency from the supplied terminal-local day count', () => {
// pickup_lfd would round to 4 UTC days (none), but the terminal-local
// count is 3 -> urgency must follow the local count and read "imminent".
const result = evaluateDemurrageUrgency(
{
pickup_lfd: '2026-02-13T12:00:00Z',
terminal_checked_at: '2026-02-09T00:00:00Z',
days_until_lfd: 3,
},
NOW,
);

expect(result.days_until_lfd).toBe(3);
expect(result.urgency).toBe('imminent');
expect(result.urgency_suppressed).toBe(false);
});

it('treats an explicit null day count as "no LFD" (urgency unknown)', () => {
const result = evaluateDemurrageUrgency(
{
pickup_lfd: '2026-02-12T00:00:00Z',
terminal_checked_at: '2026-02-09T00:00:00Z',
days_until_lfd: null,
},
NOW,
);

expect(result.days_until_lfd).toBeNull();
expect(result.urgency).toBe('unknown');
});

it('preserves null fees as null rather than coercing to an empty array', () => {
const result = evaluateDemurrageUrgency(
{
fees_at_pod_terminal: null,
terminal_checked_at: '2026-02-09T00:00:00Z',
},
NOW,
);

expect(result.fees).toBeNull();
expect(result.total_amount).toBeNull();
});
});
Loading
Loading