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
7 changes: 6 additions & 1 deletion GraphcodeKit/Sources/CLI/GraphcodeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,12 @@ public enum GraphcodeCommand: Equatable, Sendable {
])
let flags = parseFlags(arguments)
if flags["help"] != nil { throw HelpRequested() }
guard let title = flags["title"] else { throw ParseError.missingArgument("--title") }
guard let rawTitle = flags["title"] else { throw ParseError.missingArgument("--title") }
// A loop fanning work out types the name it would say out loud — "Board Visibility" —
// and the instruction to write it as one word is only ever an instruction. Folded
// here, at the boundary, so a CLI-created loop is named the way every other one is
// (see `LoopName`).
let title = LoopName.folded(rawTitle) ?? rawTitle
guard let rawType = flags["type"] else { throw ParseError.missingArgument("--type") }

let loopType: LoopType
Expand Down
23 changes: 23 additions & 0 deletions GraphcodeKit/Sources/Domain/LoopName.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

/// The one-word shape every loop name on a canvas has. A name is read in a sidebar row
/// and a card header, where one word carries further, so two concepts join in CamelCase
/// — "BoardVisibility" — instead of spending a space on them.
///
/// The backend namer asks for that shape and folds what it gets back
/// (`TitleSuggestionClient.sanitize`). This is the same fold applied where a name arrives
/// already written: `--title` on the CLI, typed by a loop fanning work out, where telling
/// is all the instruction can do.
public enum LoopName {
/// `nil` when nothing alphanumeric survives, which is the caller's cue to keep whatever
/// it was given rather than create a nameless card.
public static func folded(_ raw: String) -> String? {
let words =
raw
.split(whereSeparator: { $0.isWhitespace })
.map { $0.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) }
.filter { !$0.isEmpty }
guard !words.isEmpty else { return nil }
return words.map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined()
}
}
5 changes: 3 additions & 2 deletions GraphcodeKit/Sources/Domain/SessionBriefing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public enum SessionBriefing {
in sequence:

```sh
graphcode node create \(projectPath) --title <title> --type goal --goal <what done looks like>
graphcode node create \(projectPath) --title <OneWordName> --type goal --goal <what done looks like>
```

A **node is a loop** — the cards on graphcode's canvas are loops, and the CLI calls
Expand Down Expand Up @@ -256,7 +256,8 @@ public enum SessionBriefing {
If `graphcode` is not on your `PATH`, it is at `\(installedCLIPath)`.

Give each loop a title that says what it is for and a goal that says what done looks
like — they are what a human sees in the sidebar.
like — they are what a human sees in the sidebar. A title is one word: two concepts
join in CamelCase, `--title BoardVisibility`, never `--title "Board Visibility"`.

## When not to

Expand Down
16 changes: 15 additions & 1 deletion GraphcodeKit/Sources/Sessions/RemoteGraphAccess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,20 @@ public enum RemoteGraphAccess {
print(acknowledgement)


def folded_title(raw):
# The one-word CamelCase shape every loop name has -- LoopName.folded on the Mac.
words = []
for word in raw.split():
start, end = 0, len(word)
while start < end and not word[start].isalnum():
start += 1
while end > start and not word[end - 1].isalnum():
end -= 1
if end > start:
words.append(word[start].upper() + word[start + 1:end])
return "".join(words) or raw.strip()


def make_draft(flags):
if not flags.get("title"):
fail("missing --title")
Expand All @@ -471,7 +485,7 @@ public enum RemoteGraphAccess {
fail("a %s loop needs --%s" % (flags["type"], needed))
draft = {
"id": str(uuid.uuid4()).upper(),
"title": flags["title"],
"title": folded_title(flags["title"]),
"loopType": loop_type,
"pausesBeforeWritesOnly": False,
}
Expand Down
8 changes: 2 additions & 6 deletions graphcode/Sources/Clients/TitleSuggestionClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,8 @@ extension TitleSuggestionClient: DependencyKey {
.components(separatedBy: .newlines)
.map { $0.trimmingCharacters(in: .whitespaces) }
.last { !$0.isEmpty }
let words = (lastLine ?? "").split(separator: " ").prefix(2)
.map { $0.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) }
.filter { !$0.isEmpty }
guard !words.isEmpty else { return nil }
let name = words.map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined()
guard name.count <= 30 else { return nil }
let answer = (lastLine ?? "").split(separator: " ").prefix(2).joined(separator: " ")
guard let name = LoopName.folded(answer), name.count <= 30 else { return nil }
return name
}
}
Expand Down
20 changes: 20 additions & 0 deletions graphcode/Tests/GraphcodeCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,26 @@ extension GraphcodeCommandTests {
#expect(draft.title == "Classify")
}

@Test
func aTitleWrittenAsSeparateWordsIsFoldedIntoOne() throws {
// What a loop fanning work out actually types. Every other loop name on the canvas
// is one word, and telling the session so is all the briefing can do.
let command = try GraphcodeCommand.parse([
"node", "create", "/tmp/x", "--title", "Board Visibility", "--type", "goal",
"--goal", "boards ship",
])
guard case .createNode(_, let draft, _) = command else {
Issue.record("expected createNode, got \(command)")
return
}
#expect(draft.title == "BoardVisibility")
#expect(LoopName.folded("fix the flaky login test") == "FixTheFlakyLoginTest")
#expect(LoopName.folded("GraphCode templates") == "GraphCodeTemplates")
// Nothing alphanumeric to fold: the caller keeps what it was given rather than
// creating a nameless card.
#expect(LoopName.folded("---") == nil)
}

@Test
func compositeIsCreatableFromTheCLIUnderEitherName() throws {
// `proactive` too: that is what a composite still serialises as, so it is the word a
Expand Down
3 changes: 2 additions & 1 deletion graphcode/Tests/RemoteCLIShimTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ struct RemoteCLIShimTests {
return
}
#expect(path == Self.project)
#expect(draft.title == "Fix issue 7")
// Folded to one word, exactly as `LoopName.folded` does it on the Mac.
#expect(draft.title == "FixIssue7")
#expect(draft.loopType == .goalBased)
#expect(draft.goal?.summary == "tests pass")
#expect(draft.goal?.predicate == "make test")
Expand Down
Loading