@antoneeo/course-creator 0.1.0-beta.3 → 0.1.0-beta.5

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.
@@ -54,7 +54,7 @@ def _items(cell):
54
54
  if part.strip() and part.strip() != "-"]
55
55
 
56
56
 
57
- def _anchor_exists(path, anchor):
57
+ def _anchor_exists(path, anchor):
58
58
  if path.suffix.lower() not in (".md", ".txt"):
59
59
  if path.suffix.lower() == ".pdf":
60
60
  return bool(re.fullmatch(r"page=[1-9]\d*", anchor))
@@ -63,132 +63,132 @@ def _anchor_exists(path, anchor):
63
63
  return bool(re.fullmatch(r"\d{2}:\d{2}(?::\d{2})?", anchor))
64
64
  text = path.read_text(encoding="utf-8", errors="replace")
65
65
  return any(re.sub(r"[^a-z0-9 -]", "", heading.lower()).strip().replace(" ", "-") == anchor.lower()
66
- for heading in re.findall(r"^#+\s+(.+)$", text, re.M))
67
-
68
-
69
- def _content_contract(course_dir, plan, plan_rows, read, ref, add):
70
- """Validate authored-source structure, not its pedagogical sufficiency."""
71
- plan_path = course_dir / "COURSE_PLAN.md"
72
-
73
- def field(text, key, path, item):
74
- values = re.findall(r"^" + re.escape(key) + r":[ \t]*(.*)$", text, re.M)
75
- if len(values) != 1 or not values[0].strip() or values[0].strip() == "-":
76
- add(path, item, f"{key} must occur once with a value")
77
- return ""
78
- return values[0].strip()
79
-
80
- declarations = re.findall(r"^Content contract:[ \t]*(.*)$", plan, re.M)
81
- if not declarations:
82
- add(plan_path, "content", "legacy course: migrate to a complete textual content contract; quality is not certified", "WARN")
83
- return
84
- contract = field(plan, "Content contract", plan_path, "content")
85
- if contract not in ("slide-content-v1", "text-content-v1"):
86
- add(plan_path, "content", "unknown content contract")
87
- return
88
- prefix, name = ("S", "SLIDE_CONTENT.md") if contract == "slide-content-v1" else ("U", "COURSE_CONTENT.md")
89
- if prefix == "U":
90
- field(plan, "Format exception", plan_path, "content")
91
- path = course_dir / name
92
- text = read(path, "content")
93
- if text is None:
94
- return
95
- intro = text.split("\n## ", 1)[0]
96
- if field(intro, "Course version", path, "version") != field(plan, "Course version", plan_path, "version"):
97
- add(path, "version", "content version differs from COURSE_PLAN")
98
- if field(intro, "Delivery mode", path, "delivery") not in ("self-study", "instructor-led"):
99
- add(path, "delivery", "unknown delivery mode")
100
- for key in ("Efficacy", "Feedback flow"):
101
- if field(intro, key, path, key) != field(plan, key, plan_path, key):
102
- add(path, key, f"{key} differs from COURSE_PLAN")
103
- headers = ("profile", "baseline", "target capability", "teaching contribution", "observable check", "alternative and limits")
104
- values = _table(text, headers) or []
105
- expected_profiles = {row["profile"] for row in plan_rows}
106
- if {row["profile"] for row in values} != expected_profiles or len(values) != len(expected_profiles):
107
- add(path, "value", "Course Value must cover each taught profile exactly once")
108
- for row in values:
109
- if any(not row[h] or row[h] == "-" for h in headers):
110
- add(path, "value", "Course Value fields must be populated; semantic review still required")
111
- ref(row["observable check"], path, "value")
112
- modules = {row["module id"]: row for row in plan_rows}
113
- sections = re.split(r"(?m)^## (.+)\n", text)
114
- units = {}
115
- order = []
116
- for heading, body in zip(sections[1::2], sections[2::2]):
117
- if heading == "Course Value":
118
- continue
119
- if not re.fullmatch(prefix + r"[0-9]+", heading):
120
- add(path, heading, "unexpected unit heading")
121
- continue
122
- if heading in units:
123
- add(path, heading, "duplicate unit ID")
124
- metadata = body.split("### ", 1)[0]
125
- unit = {key: field(metadata, key, path, heading) for key in
126
- ("Module", "Objective", "Role", "Title", "Value contribution")}
127
- units[heading] = unit
128
- order.append(heading)
129
- unit["coverage"] = {(unit["Module"], unit["Objective"])}
130
- extra_coverage = re.findall(r"^Covers:[ \t]*(.*)$", metadata, re.M)
131
- if len(extra_coverage) > 1:
132
- add(path, heading, "Covers must occur at most once")
133
- if extra_coverage:
134
- pairs = _items(extra_coverage[0])
135
- if not pairs:
136
- add(path, heading, "Covers must list module/objective pairs")
137
- for pair in pairs:
138
- if not re.fullmatch(r"M\d+/O\d+", pair):
139
- add(path, heading, "invalid coverage pair; expected M1/O1")
140
- continue
141
- mid, objective = pair.split("/")
142
- if (mid, objective) in unit["coverage"]:
143
- add(path, heading, "duplicate coverage pair")
144
- unit["coverage"].add((mid, objective))
145
- for mid, objective in unit["coverage"]:
146
- if mid not in modules or modules[mid]["objective id"] != objective:
147
- add(path, heading, "coverage module/objective differs from COURSE_PLAN")
148
- module = modules.get(unit["Module"])
149
- if not module or module["objective id"] != unit["Objective"]:
150
- add(path, heading, "unit module/objective differs from COURSE_PLAN")
151
- if unit["Role"] not in ("orientation", "explanation", "check", "solution", "closing"):
152
- add(path, heading, "unknown unit role")
153
- if unit["Role"] == "check":
154
- unit["Solution"] = field(metadata, "Solution", path, heading)
155
- parts = re.split(r"(?m)^### (.+)\n", body)
156
- content = list(zip(parts[1::2], parts[2::2]))
157
- for key in ("Learner content", "Complete explanation", "Visual content", "Transition", "Sources"):
158
- blocks = [b.strip() for k, b in content if k == key]
159
- if len(blocks) != 1 or not blocks[0] or blocks[0] == "-":
160
- add(path, heading, f"{key} section must occur once with content")
161
- for key, block in content:
162
- if key == "Sources":
163
- for source in block.strip().splitlines():
164
- ref(source.removeprefix("- ").strip(), path, heading, allow_url=True)
165
- source_path = path.relative_to(course_dir.parents[3]).as_posix()
166
- for mid, row in modules.items():
167
- for key, roles in (("explanation", {"orientation", "explanation"}), ("check", {"check"})):
168
- references = _items(row[key])
169
- if not references:
170
- continue # existing module checker reports missing fields
171
- for value in references:
172
- target, _, anchor = value.partition("#")
173
- uid = anchor.upper()
174
- unit = units.get(uid, {})
175
- if (target != source_path or (mid, row["objective id"]) not in unit.get("coverage", set())
176
- or unit.get("Role") not in roles):
177
- add(plan_path, mid, f"{key} must locate its module's canonical content unit")
178
- for uid, unit in units.items():
179
- if unit["Role"] == "check":
180
- solution_id = unit.get("Solution", "")
181
- solution = units.get(solution_id, {})
182
- if (solution.get("Role") != "solution"
183
- or not unit["coverage"].issubset(solution.get("coverage", set()))
184
- or order.index(solution_id) <= order.index(uid)):
185
- add(path, uid, "check needs a subsequent solution covering every module/objective")
186
- for row in values:
187
- target, _, anchor = row["observable check"].partition("#")
188
- unit = units.get(anchor.upper(), {})
189
- covered_profiles = {modules[mid]["profile"] for mid, obj in unit.get("coverage", set()) if mid in modules}
190
- if target != source_path or unit.get("Role") != "check" or row["profile"] not in covered_profiles:
191
- add(path, "value", "observable check must locate a canonical check for its profile")
66
+ for heading in re.findall(r"^#+\s+(.+)$", text, re.M))
67
+
68
+
69
+ def _content_contract(course_dir, plan, plan_rows, read, ref, add):
70
+ """Validate authored-source structure, not its pedagogical sufficiency."""
71
+ plan_path = course_dir / "COURSE_PLAN.md"
72
+
73
+ def field(text, key, path, item):
74
+ values = re.findall(r"^" + re.escape(key) + r":[ \t]*(.*)$", text, re.M)
75
+ if len(values) != 1 or not values[0].strip() or values[0].strip() == "-":
76
+ add(path, item, f"{key} must occur once with a value")
77
+ return ""
78
+ return values[0].strip()
79
+
80
+ declarations = re.findall(r"^Content contract:[ \t]*(.*)$", plan, re.M)
81
+ if not declarations:
82
+ add(plan_path, "content", "legacy course: migrate to a complete textual content contract; quality is not certified", "WARN")
83
+ return
84
+ contract = field(plan, "Content contract", plan_path, "content")
85
+ if contract not in ("slide-content-v1", "text-content-v1"):
86
+ add(plan_path, "content", "unknown content contract")
87
+ return
88
+ prefix, name = ("S", "SLIDE_CONTENT.md") if contract == "slide-content-v1" else ("U", "COURSE_CONTENT.md")
89
+ if prefix == "U":
90
+ field(plan, "Format exception", plan_path, "content")
91
+ path = course_dir / name
92
+ text = read(path, "content")
93
+ if text is None:
94
+ return
95
+ intro = text.split("\n## ", 1)[0]
96
+ if field(intro, "Course version", path, "version") != field(plan, "Course version", plan_path, "version"):
97
+ add(path, "version", "content version differs from COURSE_PLAN")
98
+ if field(intro, "Delivery mode", path, "delivery") not in ("self-study", "instructor-led"):
99
+ add(path, "delivery", "unknown delivery mode")
100
+ for key in ("Efficacy", "Feedback flow"):
101
+ if field(intro, key, path, key) != field(plan, key, plan_path, key):
102
+ add(path, key, f"{key} differs from COURSE_PLAN")
103
+ headers = ("profile", "baseline", "target capability", "teaching contribution", "observable check", "alternative and limits")
104
+ values = _table(text, headers) or []
105
+ expected_profiles = {row["profile"] for row in plan_rows}
106
+ if {row["profile"] for row in values} != expected_profiles or len(values) != len(expected_profiles):
107
+ add(path, "value", "Course Value must cover each taught profile exactly once")
108
+ for row in values:
109
+ if any(not row[h] or row[h] == "-" for h in headers):
110
+ add(path, "value", "Course Value fields must be populated; semantic review still required")
111
+ ref(row["observable check"], path, "value")
112
+ modules = {row["module id"]: row for row in plan_rows}
113
+ sections = re.split(r"(?m)^## (.+)\n", text)
114
+ units = {}
115
+ order = []
116
+ for heading, body in zip(sections[1::2], sections[2::2]):
117
+ if heading == "Course Value":
118
+ continue
119
+ if not re.fullmatch(prefix + r"[0-9]+", heading):
120
+ add(path, heading, "unexpected unit heading")
121
+ continue
122
+ if heading in units:
123
+ add(path, heading, "duplicate unit ID")
124
+ metadata = body.split("### ", 1)[0]
125
+ unit = {key: field(metadata, key, path, heading) for key in
126
+ ("Module", "Objective", "Role", "Title", "Value contribution")}
127
+ units[heading] = unit
128
+ order.append(heading)
129
+ unit["coverage"] = {(unit["Module"], unit["Objective"])}
130
+ extra_coverage = re.findall(r"^Covers:[ \t]*(.*)$", metadata, re.M)
131
+ if len(extra_coverage) > 1:
132
+ add(path, heading, "Covers must occur at most once")
133
+ if extra_coverage:
134
+ pairs = _items(extra_coverage[0])
135
+ if not pairs:
136
+ add(path, heading, "Covers must list module/objective pairs")
137
+ for pair in pairs:
138
+ if not re.fullmatch(r"M\d+/O\d+", pair):
139
+ add(path, heading, "invalid coverage pair; expected M1/O1")
140
+ continue
141
+ mid, objective = pair.split("/")
142
+ if (mid, objective) in unit["coverage"]:
143
+ add(path, heading, "duplicate coverage pair")
144
+ unit["coverage"].add((mid, objective))
145
+ for mid, objective in unit["coverage"]:
146
+ if mid not in modules or modules[mid]["objective id"] != objective:
147
+ add(path, heading, "coverage module/objective differs from COURSE_PLAN")
148
+ module = modules.get(unit["Module"])
149
+ if not module or module["objective id"] != unit["Objective"]:
150
+ add(path, heading, "unit module/objective differs from COURSE_PLAN")
151
+ if unit["Role"] not in ("orientation", "explanation", "check", "solution", "closing"):
152
+ add(path, heading, "unknown unit role")
153
+ if unit["Role"] == "check":
154
+ unit["Solution"] = field(metadata, "Solution", path, heading)
155
+ parts = re.split(r"(?m)^### (.+)\n", body)
156
+ content = list(zip(parts[1::2], parts[2::2]))
157
+ for key in ("Learner content", "Complete explanation", "Visual content", "Transition", "Sources"):
158
+ blocks = [b.strip() for k, b in content if k == key]
159
+ if len(blocks) != 1 or not blocks[0] or blocks[0] == "-":
160
+ add(path, heading, f"{key} section must occur once with content")
161
+ for key, block in content:
162
+ if key == "Sources":
163
+ for source in block.strip().splitlines():
164
+ ref(source.removeprefix("- ").strip(), path, heading, allow_url=True)
165
+ source_path = path.relative_to(course_dir.parents[3]).as_posix()
166
+ for mid, row in modules.items():
167
+ for key, roles in (("explanation", {"orientation", "explanation"}), ("check", {"check"})):
168
+ references = _items(row[key])
169
+ if not references:
170
+ continue # existing module checker reports missing fields
171
+ for value in references:
172
+ target, _, anchor = value.partition("#")
173
+ uid = anchor.upper()
174
+ unit = units.get(uid, {})
175
+ if (target != source_path or (mid, row["objective id"]) not in unit.get("coverage", set())
176
+ or unit.get("Role") not in roles):
177
+ add(plan_path, mid, f"{key} must locate its module's canonical content unit")
178
+ for uid, unit in units.items():
179
+ if unit["Role"] == "check":
180
+ solution_id = unit.get("Solution", "")
181
+ solution = units.get(solution_id, {})
182
+ if (solution.get("Role") != "solution"
183
+ or not unit["coverage"].issubset(solution.get("coverage", set()))
184
+ or order.index(solution_id) <= order.index(uid)):
185
+ add(path, uid, "check needs a subsequent solution covering every module/objective")
186
+ for row in values:
187
+ target, _, anchor = row["observable check"].partition("#")
188
+ unit = units.get(anchor.upper(), {})
189
+ covered_profiles = {modules[mid]["profile"] for mid, obj in unit.get("coverage", set()) if mid in modules}
190
+ if target != source_path or unit.get("Role") != "check" or row["profile"] not in covered_profiles:
191
+ add(path, "value", "observable check must locate a canonical check for its profile")
192
192
 
193
193
 
194
194
  def validate_course(root, course_dir):
@@ -448,11 +448,11 @@ def validate_course(root, course_dir):
448
448
  else:
449
449
  add(course_dir / "COURSE_PLAN.md", "efficacy",
450
450
  "human review of efficacy evidence required", severity="WARN")
451
- _content_contract(course_dir, plan, plan_rows, read, ref, add)
452
- return findings
453
-
454
-
455
- def validate_all(root):
451
+ _content_contract(course_dir, plan, plan_rows, read, ref, add)
452
+ return findings
453
+
454
+
455
+ def validate_all(root):
456
456
  root = Path(root).resolve()
457
457
  docs = root / "ai_docs"
458
458
  parent = docs / "solutions/courses"
@@ -125,6 +125,21 @@ def kb_unquote(value):
125
125
  return v
126
126
 
127
127
 
128
+ def frontmatter_date(meta):
129
+ """A note's or a sidecar's `date:` as `YYYY-MM-DD`, or None (F-073).
130
+ Quotes stripped, a datetime suffix tolerated, an impossible date refused --
131
+ the one parser behind orient's recency probe and any chronology reader."""
132
+ m = re.match(r"(\d{4}-\d{2}-\d{2})", kb_unquote((meta or {}).get("date")))
133
+ if not m:
134
+ return None
135
+ try:
136
+ import datetime as _dt
137
+ _dt.date.fromisoformat(m.group(1))
138
+ except ValueError:
139
+ return None
140
+ return m.group(1)
141
+
142
+
128
143
  def kb_parse_scope(text):
129
144
  """'-' | 'from X' | 'until X' | 'from X until Y' | 'if <cond>' ->
130
145
  ('always',) | ('window', from, until) | ('if', cond). Raises on junk."""
@@ -336,6 +351,17 @@ def kb_check_claims(root):
336
351
  if rel not in fm_cache:
337
352
  fm_cache[rel] = _note_frontmatter(root, rel)
338
353
  return fm_cache[rel]
354
+ # F-069: the same per-run memo for the source paths themselves -- each
355
+ # distinct path is resolved and stat'ed once, and each cited extraction
356
+ # read once (loc_cache, owned here, dropped when this call returns).
357
+ src_cache = {}
358
+ loc_cache = {}
359
+
360
+ def _resolved(path_s):
361
+ if path_s not in src_cache:
362
+ t = sdlc_core.confine_under(root, path_s)
363
+ src_cache[path_s] = (t, t is not None and t.is_file())
364
+ return src_cache[path_s]
339
365
  per_file_rows = []
340
366
  for p in sorted(topics.glob("*.md")):
341
367
  rel = "topics/" + p.name
@@ -357,16 +383,16 @@ def kb_check_claims(root):
357
383
  errors.append("%s: source %r has no locator" % (where, src))
358
384
  continue
359
385
  path_s, loc = src.rsplit("#", 1)
360
- target = sdlc_core.confine_under(root, path_s)
386
+ target, is_file = _resolved(path_s)
361
387
  if target is None:
362
388
  errors.append("%s: source %r escapes the docs root" % (where, src))
363
389
  continue
364
- if not target.is_file():
390
+ if not is_file:
365
391
  errors.append("%s: source %r does not resolve — a claim whose "
366
392
  "origin cannot be reopened is model knowledge"
367
393
  % (where, src))
368
394
  continue
369
- kb_check_locator(target, loc, where, errors)
395
+ kb_check_locator(target, loc, where, errors, loc_cache)
370
396
  # --- provenance ---
371
397
  prov = row["prov"]
372
398
  # Resolved ONCE, for every provenance: the non-GIVEN classes read it
@@ -556,8 +582,14 @@ def kb_check_claims(root):
556
582
  return errors, warnings, notes
557
583
 
558
584
 
559
- def kb_check_locator(target, loc, where, errors):
560
- """A locator must address an existing span of the stored bytes."""
585
+ def kb_check_locator(target, loc, where, errors, cache=None):
586
+ """A locator must address an existing span of the stored bytes.
587
+
588
+ `cache` is an optional dict the CALLER owns for one run (F-069): keyed by
589
+ the path actually read, it holds only what the checks compare against --
590
+ page lengths, a line count, or False for a missing extraction -- never the
591
+ text. Findings are still appended per call, so per row. Without it, every
592
+ call reads the file, as before."""
561
593
  m = LOC_PAGE_RE.match(loc)
562
594
  if m:
563
595
  page, a, b = int(m.group(1)), int(m.group(2)), int(m.group(3))
@@ -566,17 +598,24 @@ def kb_check_locator(target, loc, where, errors):
566
598
  return
567
599
  # offsets address the stored extraction (<stem>.txt beside the original)
568
600
  ext = target if target.suffix == ".txt" else target.with_suffix(".txt")
569
- if not ext.is_file():
601
+ key = ("p", ext)
602
+ if cache is None or key not in cache:
603
+ lengths = ([len(pg) for pg in sdlc_core.read_text(ext).split("\f")]
604
+ if ext.is_file() else False)
605
+ if cache is not None:
606
+ cache[key] = lengths
607
+ else:
608
+ lengths = cache[key]
609
+ if lengths is False:
570
610
  errors.append("%s: no stored extraction %s for locator %r — offsets "
571
611
  "must address kept bytes" % (where, ext.name, loc))
572
612
  return
573
- pages = sdlc_core.read_text(ext).split("\f")
574
- if page < 1 or page > len(pages):
613
+ if page < 1 or page > len(lengths):
575
614
  errors.append("%s: locator %r addresses page %d of %d" %
576
- (where, loc, page, len(pages)))
577
- elif b > len(pages[page - 1]):
615
+ (where, loc, page, len(lengths)))
616
+ elif b > lengths[page - 1]:
578
617
  errors.append("%s: locator %r spans past the end of page %d "
579
- "(%d chars)" % (where, loc, page, len(pages[page - 1])))
618
+ "(%d chars)" % (where, loc, page, lengths[page - 1]))
580
619
  return
581
620
  m = LOC_LINE_RE.match(loc)
582
621
  if m:
@@ -584,7 +623,13 @@ def kb_check_locator(target, loc, where, errors):
584
623
  if a > b or a < 1:
585
624
  errors.append("%s: locator %r has an invalid line range" % (where, loc))
586
625
  return
587
- n = sdlc_core.read_text(target).count("\n") + 1
626
+ key = ("L", target)
627
+ if cache is None or key not in cache:
628
+ n = sdlc_core.read_text(target).count("\n") + 1
629
+ if cache is not None:
630
+ cache[key] = n
631
+ else:
632
+ n = cache[key]
588
633
  if b > n:
589
634
  errors.append("%s: locator %r spans past line %d" % (where, loc, n))
590
635
  return
@@ -1085,12 +1130,18 @@ KB_GENERATED_RE = re.compile(r"GENERATED by \S+ index - do not edit by hand")
1085
1130
  KB_HEX12_RE = re.compile(r"^[0-9a-f]{12}$")
1086
1131
 
1087
1132
 
1088
- def _kb_hex_bounded(text, cid):
1089
- """True when cid appears NOT embedded in a longer hex run: a git hash
1090
- sharing a substring must never count as a citation (either side, either
1091
- case). cid is pre-validated 12-hex, so it is regex-safe by construction."""
1092
- return re.search(r"(?<![0-9a-fA-F])%s(?![0-9a-fA-F])" % cid,
1093
- text) is not None
1133
+ # A citation of a claim id is a run of exactly its 12 lowercase hex characters
1134
+ # NOT embedded in a longer hex run: a git hash sharing a substring must never
1135
+ # count as a citation (either side, either case). Two such bounded runs cannot
1136
+ # overlap, so findall returns every one of them.
1137
+ KB_HEX_TOKEN_RE = re.compile(r"(?<![0-9a-fA-F])[0-9a-f]{12}(?![0-9a-fA-F])")
1138
+
1139
+
1140
+ def _kb_hex_tokens(text):
1141
+ """The ids a text cites, extracted in one pass (F-069). Callers test
1142
+ membership instead of scanning the text once per id: that per-(document,
1143
+ id) scan made the time cycle grow with superseded ids x living documents."""
1144
+ return frozenset(KB_HEX_TOKEN_RE.findall(text))
1094
1145
 
1095
1146
 
1096
1147
  def _kb_df_entries(value):
@@ -1172,19 +1223,23 @@ def kb_time_cycle(docs):
1172
1223
  if (meta.get("status") or "").strip() == "SUPERSEDED":
1173
1224
  continue
1174
1225
  living.append((rel, text))
1175
- for rel, text in living:
1176
- for old in sorted(superseded):
1177
- if _kb_hex_bounded(text, old):
1178
- succs = superseded[old]
1179
- missing = tuple(x for x in succs
1180
- if not _kb_hex_bounded(text, x))
1181
- if missing:
1182
- cited.append((rel, old, missing, succs))
1226
+ # One token pass per living document, and none at all on a KB with no
1227
+ # supersession and no validity window (the orient line runs this at every
1228
+ # session start).
1229
+ tokens = ({rel: _kb_hex_tokens(text) for rel, text in living}
1230
+ if superseded or windows else {})
1231
+ for rel, _text in living:
1232
+ cites = tokens[rel] if tokens else frozenset()
1233
+ for old in cites.intersection(superseded):
1234
+ succs = superseded[old]
1235
+ missing = tuple(x for x in succs if x not in cites)
1236
+ if missing:
1237
+ cited.append((rel, old, missing, succs))
1183
1238
  today = _dt.date.today().isoformat()
1184
1239
  for topic_rel, rid, until in sorted(windows):
1185
1240
  if until <= today:
1186
- by = tuple(rel for rel, text in living
1187
- if rel != topic_rel and _kb_hex_bounded(text, rid))
1241
+ by = tuple(rel for rel, _text in living
1242
+ if rel != topic_rel and rid in tokens[rel])
1188
1243
  expired.append((topic_rel, rid, until, by))
1189
1244
  rev = {}
1190
1245
  node_meta = {}
@@ -1905,14 +1960,13 @@ def kb_cmd_orient(argv):
1905
1960
  # it -- the file was already fully read, so the cap bought
1906
1961
  # only failure. load_frontmatter is the authoritative
1907
1962
  # reader (fence-bounded, so a column-0 date: in the body
1908
- # still cannot misdate the note); kb_unquote covers quoted
1909
- # dates; the prefix match tolerates a datetime suffix.
1963
+ # still cannot misdate the note); frontmatter_date covers
1964
+ # quoted dates and tolerates a datetime suffix.
1910
1965
  meta = sdlc_core.load_frontmatter(
1911
1966
  sdlc_core.read_text(p).splitlines()) or {}
1912
- m = re.match(r"(\d{4}-\d{2}-\d{2})",
1913
- kb_unquote(meta.get("date")))
1914
- if m:
1915
- stamp = _dt.date.fromisoformat(m.group(1))
1967
+ day = frontmatter_date(meta)
1968
+ if day:
1969
+ stamp = _dt.date.fromisoformat(day)
1916
1970
  except Exception:
1917
1971
  pass
1918
1972
  if stamp is None: