feat(zddc): add ParseTransmittalFolder + IsTrnOrSubTracking helpers
Extracts the YYYY-MM-DD_<tracking> (<status>) - <title> grammar into a reusable parser in the zddc package, and exposes a tracking-type predicate for -TRN- / -SUB- (case-fold). The transmittal-folder regex was previously only inside archive/index.go where it captured just the date; the new ParseTransmittalFolder also returns tracking, status, and title so handlers can recognise transmittal envelopes for upcoming staging↔working mirror logic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f450cdaf87
commit
5363b5364c
2 changed files with 137 additions and 0 deletions
48
zddc/internal/zddc/folder.go
Normal file
48
zddc/internal/zddc/folder.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package zddc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// transmittalFolderRE matches the canonical ZDDC transmittal-folder shape:
|
||||||
|
//
|
||||||
|
// YYYY-MM-DD_<tracking> (<status>) - <title>
|
||||||
|
//
|
||||||
|
// where <tracking> has no spaces or underscores, <status> is anything inside
|
||||||
|
// parentheses, and <title> is anything after the dash. Capture groups:
|
||||||
|
//
|
||||||
|
// 1: date (YYYY-MM-DD)
|
||||||
|
// 2: tracking number (e.g. proj-EM-TRN-0042)
|
||||||
|
// 3: status (e.g. IFR, IFA, RSA, DFT)
|
||||||
|
// 4: title
|
||||||
|
var transmittalFolderRE = regexp.MustCompile(
|
||||||
|
`^(\d{4}-\d{2}-\d{2})_([^_\s]+(?:-[^_\s]+)*)\s*\(([^)]+)\)\s*-\s*(.+)$`,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseTransmittalFolder splits a folder basename into its ZDDC transmittal
|
||||||
|
// components. The fourth return is true iff name is a well-formed transmittal
|
||||||
|
// folder. Trailing slashes on name are tolerated.
|
||||||
|
//
|
||||||
|
// Used by handlers that need to recognise a staging-folder mkdir as a
|
||||||
|
// transmittal envelope (to mirror it under working/), and by the archive
|
||||||
|
// indexer that scans for transmittal folders on disk.
|
||||||
|
func ParseTransmittalFolder(name string) (date, tracking, status, title string, ok bool) {
|
||||||
|
name = strings.TrimRight(name, "/")
|
||||||
|
m := transmittalFolderRE.FindStringSubmatch(name)
|
||||||
|
if m == nil {
|
||||||
|
return "", "", "", "", false
|
||||||
|
}
|
||||||
|
return m[1], m[2], m[3], m[4], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTrnOrSubTracking reports whether tracking contains a "-TRN-" or "-SUB-"
|
||||||
|
// segment (case-insensitive). These two tracking types are the ones whose
|
||||||
|
// staging folders get a paired drafting folder under working/.
|
||||||
|
//
|
||||||
|
// "-MDL-" and other tracking types do NOT match — MDL deliverables are
|
||||||
|
// tracked via per-party mdl/ rows, not via the working↔staging mirror.
|
||||||
|
func IsTrnOrSubTracking(tracking string) bool {
|
||||||
|
upper := strings.ToUpper(tracking)
|
||||||
|
return strings.Contains(upper, "-TRN-") || strings.Contains(upper, "-SUB-")
|
||||||
|
}
|
||||||
89
zddc/internal/zddc/folder_test.go
Normal file
89
zddc/internal/zddc/folder_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package zddc
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseTransmittalFolder(t *testing.T) {
|
||||||
|
type expect struct {
|
||||||
|
date, tracking, status, title string
|
||||||
|
ok bool
|
||||||
|
}
|
||||||
|
cases := map[string]expect{
|
||||||
|
"2026-06-15_proj-EM-TRN-0042 (DFT) - Foundation Plans": {
|
||||||
|
date: "2026-06-15", tracking: "proj-EM-TRN-0042", status: "DFT", title: "Foundation Plans", ok: true,
|
||||||
|
},
|
||||||
|
"2026-01-15_123456-EM-SUB-0001 (IFR) - Submittal Title.with.dots": {
|
||||||
|
date: "2026-01-15", tracking: "123456-EM-SUB-0001", status: "IFR", title: "Submittal Title.with.dots", ok: true,
|
||||||
|
},
|
||||||
|
"2026-01-15_proj-EM-SUB-0001 (RSA) - Response Notes": {
|
||||||
|
date: "2026-01-15", tracking: "proj-EM-SUB-0001", status: "RSA", title: "Response Notes", ok: true,
|
||||||
|
},
|
||||||
|
// Whitespace variations around "(" and "-".
|
||||||
|
"2026-01-15_proj-EM-TRN-1(IFC)-Title": {
|
||||||
|
date: "2026-01-15", tracking: "proj-EM-TRN-1", status: "IFC", title: "Title", ok: true,
|
||||||
|
},
|
||||||
|
// MDL is a valid transmittal name even though it doesn't match TRN/SUB.
|
||||||
|
"2026-01-15_proj-EM-MDL-0001 (IFR) - Master Deliverables List": {
|
||||||
|
date: "2026-01-15", tracking: "proj-EM-MDL-0001", status: "IFR", title: "Master Deliverables List", ok: true,
|
||||||
|
},
|
||||||
|
// Negatives.
|
||||||
|
"": {ok: false},
|
||||||
|
"scratch": {ok: false},
|
||||||
|
"2026-06-15 missing-underscore": {ok: false},
|
||||||
|
"NotADate_proj-EM-TRN-0042 (DFT) - Title": {ok: false},
|
||||||
|
"2026-06-15_no-status - Title": {ok: false},
|
||||||
|
"2026-06-15_no-dash (IFC) Title": {ok: false},
|
||||||
|
// Tracking with whitespace must not parse — tracking has no spaces.
|
||||||
|
"2026-06-15_proj EM TRN (IFC) - Title": {ok: false},
|
||||||
|
}
|
||||||
|
for in, want := range cases {
|
||||||
|
date, tracking, status, title, ok := ParseTransmittalFolder(in)
|
||||||
|
if ok != want.ok {
|
||||||
|
t.Errorf("ParseTransmittalFolder(%q) ok=%v, want %v", in, ok, want.ok)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if date != want.date || tracking != want.tracking || status != want.status || title != want.title {
|
||||||
|
t.Errorf("ParseTransmittalFolder(%q) = (%q,%q,%q,%q), want (%q,%q,%q,%q)",
|
||||||
|
in, date, tracking, status, title, want.date, want.tracking, want.status, want.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTransmittalFolderTrailingSlash(t *testing.T) {
|
||||||
|
date, tracking, _, _, ok := ParseTransmittalFolder("2026-06-15_proj-TRN-1 (DFT) - x/")
|
||||||
|
if !ok || date != "2026-06-15" || tracking != "proj-TRN-1" {
|
||||||
|
t.Errorf("trailing slash not tolerated: ok=%v date=%q tracking=%q", ok, date, tracking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsTrnOrSubTracking(t *testing.T) {
|
||||||
|
yes := []string{
|
||||||
|
"proj-EM-TRN-0042",
|
||||||
|
"proj-EM-SUB-0001",
|
||||||
|
"PROJ-em-trn-0001", // case-fold
|
||||||
|
"vendor-sub-9",
|
||||||
|
"a-TRN-b-SUB-c", // either qualifies
|
||||||
|
}
|
||||||
|
no := []string{
|
||||||
|
"",
|
||||||
|
"proj-EM-MDL-0001",
|
||||||
|
"trn-without-dashes",
|
||||||
|
"sub-without-dashes",
|
||||||
|
"prefixTRN", // no dashes around TRN
|
||||||
|
"-TRN", // missing trailing dash
|
||||||
|
"TRN-", // missing leading dash
|
||||||
|
"proj-TRNX-001", // not exactly -TRN-
|
||||||
|
}
|
||||||
|
for _, s := range yes {
|
||||||
|
if !IsTrnOrSubTracking(s) {
|
||||||
|
t.Errorf("IsTrnOrSubTracking(%q) = false, want true", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range no {
|
||||||
|
if IsTrnOrSubTracking(s) {
|
||||||
|
t.Errorf("IsTrnOrSubTracking(%q) = true, want false", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue