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
2 changes: 1 addition & 1 deletion codepet/Managers/CompanyStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2173,7 +2173,7 @@ final class CompanyStore: ObservableObject {
// What the founder pinned on the `+` menu, rendered above the
// ranker's guesses and excluded from them. Empty for every
// caller that passes nothing.
pinned: pinned, departmentWork: deskWork),
pinned: pinned, departmentWork: deskWork, today: Date()),
history: Array(history), userMessage: text, runnable: Array(runnable),
openTasks: Array(openTasks), envSetup: envSetup,
// nil at defaults, so an untouched settings panel adds nothing to the wire
Expand Down
18 changes: 17 additions & 1 deletion codepet/Models/ChatContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ enum ChatContext {
/// Render a compact per-department status snapshot — one line per department that
/// has at least one task assigned (fully-untouched departments are skipped), mirroring
/// web's deptSummary (`- name (status, N to do): focus`).
/// "Today is 2026-10-09 (October 2026)." — ISO for arithmetic, the month name so "8 months
/// from now" lands on a month the model can name without converting.
static func composeToday(_ date: Date) -> String {
let iso = DateFormatter(), month = DateFormatter()
for f in [iso, month] { f.locale = Locale(identifier: "en_US_POSIX"); f.calendar = Calendar(identifier: .gregorian) }
iso.dateFormat = "yyyy-MM-dd"
month.dateFormat = "LLLL yyyy"
return "Today is \(iso.string(from: date)) (\(month.string(from: date))). Count any deadline or runway from this date."
}

/// How many of the department's own deliverables the desk excerpts, newest first.
static let deskWorkCap = 4
/// How many of the department's own notes (decisions tagged with its `dept`) the desk lists.
Expand Down Expand Up @@ -312,8 +322,14 @@ enum ChatContext {
product: String? = nil,
library: [Deliverable] = [], query: String? = nil,
focusDepartment: Department? = nil, memoryEnabled: Bool = true,
pinned: [ContextPin] = [], departmentWork: [Deliverable] = []) -> String {
pinned: [ContextPin] = [], departmentWork: [Deliverable] = [],
today: Date? = nil) -> String {
var parts: [String] = []
// First, so the backend's CONTEXT_CAP clip can never trim it. 9 Oct: told "runway is
// 8 months" on 9 Oct 2026, Finance dated the deadline "June 2026" — nothing in the
// context said what today was. Here rather than in the cached system prompt, which
// must stay identical across days for the prefix cache to hit.
if let today { parts.append(composeToday(today)) }
// Memory off composes nothing from `decisions` — the desk's notes included.
// `decisions` is already the applicable set (`CompanyStore.applicableDecisions`), so the
// desk can never show another project's note: it reads only this parameter.
Expand Down
12 changes: 12 additions & 0 deletions codepetTests/ChatContextDepartmentDeskTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,16 @@ final class ChatContextDepartmentDeskTests: XCTestCase {
let out = ChatContext.compose(brief: brief, tasks: [], decisions: ds, focusDepartment: finance)
XCTAssertTrue(out.contains("Sales says $29"))
}

/// End to end through the merge: once Sales re-records Finance's price, Finance's desk no
/// longer lists it and Sales' does.
func testAReRecordedTopicMovesDesks() {
let fin = Decisions.mergeDecisions(existing: [], extracted: [ExtractedDecision(topic: "pricing", statement: "$19", source: nil)],
now: 1, dept: "fin")
let both = Decisions.mergeDecisions(existing: fin, extracted: [ExtractedDecision(topic: "pricing", statement: "$29", source: nil)],
now: 2, dept: "sales")
XCTAssertEqual(notes(in: ChatContext.compose(brief: brief, tasks: [], decisions: both, focusDepartment: finance)), [])
XCTAssertEqual(notes(in: ChatContext.compose(brief: brief, tasks: [], decisions: both, focusDepartment: sales), "Sales"),
["- pricing: $29"])
}
}
28 changes: 28 additions & 0 deletions codepetTests/ChatContextTodayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// codepetTests/ChatContextTodayTests.swift
import XCTest
@testable import codepet

/// 9 Oct: told "runway is 8 months" on 9 Oct 2026, Finance put the deadline at "June 2026".
/// Nothing in the Ask context said what today was, so every date the model computed floated.
final class ChatContextTodayTests: XCTestCase {
private func day(_ y: Int, _ m: Int, _ d: Int) -> Date {
var c = DateComponents(); c.year = y; c.month = m; c.day = d; c.hour = 12
return Calendar(identifier: .gregorian).date(from: c)!
}

func testTheContextSaysWhatDayItIs() {
let out = ChatContext.compose(brief: CompanyBrief(), tasks: [], today: day(2026, 10, 9))
XCTAssertTrue(out.contains("Today is 2026-10-09 (October 2026)"), out)
}

/// First, so a clip of the context (CONTEXT_CAP) can never trim it away.
func testTheDateLeadsTheContext() {
let out = ChatContext.compose(brief: CompanyBrief(projectName: "Codepet"), tasks: [], today: day(2026, 10, 9))
XCTAssertTrue(out.hasPrefix("Today is 2026-10-09"))
}

/// Callers that pass no date are unchanged.
func testNoDateWhenNoneIsGiven() {
XCTAssertFalse(ChatContext.compose(brief: CompanyBrief(), tasks: []).contains("Today is"))
}
}
21 changes: 21 additions & 0 deletions codepetTests/CompanyStoreChatTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -492,10 +492,31 @@ final class CompanyStoreChatTests: XCTestCase {
chatStreamer: Self.streamer(deltas: ["Noted"], remember: [fact]),
decisionsSaver: { _, _ in true })
await s.hydrate(companyId: "u")
// A Finance turn first: the ordinary turn after it must not inherit the department —
// and re-recording the same topic without one moves it off Finance's desk.
await s.sendChat("ten paying founders is the goal", language: .en, department: DepartmentCatalog.find("fin"))
XCTAssertEqual(s.company.decisions.first { $0.topic == "goal" }?.dept, "fin")
await s.sendChat("ten paying founders is the goal", language: .en)
XCTAssertNil(s.company.decisions.first { $0.topic == "goal" }?.dept)
}

/// The request's context says what day it is — the compose tests alone pass even if the
/// store stops passing a date. 9 Oct: Finance dated an 8-month runway "June 2026".
func testTheChatRequestCarriesTodaysDate() async {
var context = ""
let streamer: (CompanyChatRequest) -> AsyncThrowingStream<CompanyChatStreamEvent, Error> = { req in
context = req.context
return AsyncThrowingStream { c in
c.yield(.delta("hi")); c.yield(.done(model: "m", cacheHit: false, action: ChatDoneAction())); c.finish()
}
}
let s = CompanyStore(loader: { _ in .empty }, saver: { _, _ in true },
chatSender: { _ in nil }, chatStreamer: streamer)
await s.hydrate(companyId: "u")
await s.sendChat("how long is our runway?", language: .en)
XCTAssertTrue(context.hasPrefix(ChatContext.composeToday(Date())), context)
}

/// `sendChat` populates `env_setup` from the company's currently-OFF toolkit
/// items (mirrors the web's off-toolkit filter) — AND, since the
/// environment-honesty pass, only from items that are actually built.
Expand Down
9 changes: 9 additions & 0 deletions codepetTests/DepartmentNotebookTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,13 @@ final class DepartmentNotebookTests: XCTestCase {
let out = String(data: try JSONEncoder().encode(d), encoding: .utf8)!
XCTAssertFalse(out.contains("dept"))
}

/// A company-wide re-record (a room lock-in, an ordinary turn) takes the topic off the
/// department's desk: one truth per topic, and that truth is now nobody's in particular.
func testReRecordingWithoutADepartmentClearsTheTag() {
let a = Decisions.mergeDecisions(existing: [], extracted: [x("pricing", "$19")], now: 1, dept: "fin")
let b = Decisions.mergeDecisions(existing: a, extracted: [x("pricing", "$24")], now: 2)
XCTAssertEqual(b.count, 1)
XCTAssertNil(b.first?.dept)
}
}
Loading