@antoneeo/agentic-sdlc-skill 1.39.0 → 1.41.0
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.
- package/CHANGELOG.md +11 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/init.js +6 -6
- package/skills/agentic-sdlc-skill/ENFORCEMENT.md +5 -5
- package/skills/agentic-sdlc-skill/SKILL.md +3 -3
- package/skills/agentic-sdlc-skill/debugging.md +5 -5
- package/skills/agentic-sdlc-skill/scripts/knowledge.py +88 -34
- package/skills/agentic-sdlc-skill/scripts/sdlc_core.py +358 -6
- package/skills/agentic-sdlc-skill/templates.md +19 -3
- package/skills/agentic-sdlc-skill/vision.md +245 -245
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Tutte le modifiche significative a questa skill saranno documentate in questo file.
|
|
4
4
|
|
|
5
|
+
## [1.41.0] - 2026-10-17
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- F-073: one written form for a dated entry, `**YYYY-MM-DD — label.**`, is taught for Diary entries, spike outcomes and project notes that record an event, and shared readers (`entry_openers`, `request_entries`, `adr_date`, `frontmatter_date`) let a chronology list those events the same way the validator reads them. Three new advisories, which never fail a build, not even `--strict`: undated text in the Diary of an L3 analysis started on or after 2026-10-18; a HANDOFF `next:` longer than 240 characters on an L3 file updated on or after that date (`next:` now holds only the next step, and the state of the work goes to the Diary); and, in `check` only, an open L3 unit whose id another branch or tag uses for a different analysis (one `git grep` over local refs, silent without git). The doctrine now says to choose a new unit's id after fetching, and that a session sharing a checkout with another session works in its own git worktree.
|
|
9
|
+
|
|
10
|
+
## [1.40.0] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- F1: closing the last open workstream now empties the generated registry `audit/handoff.md` instead of leaving its stale row behind: with zero `HANDOFF_*.md` sources `index` rewrites it empty (keeping `project_notes.md`), and `validate` reports a generated registry that still has rows but no sources as an error.
|
|
14
|
+
- F-069: `graph`, `check`, `stale` and the session-start count are faster on a mature knowledge base, with byte-identical output. Citations of superseded claim ids are found with one pass per living document instead of one scan per (document, id) pair, and one check run reads and resolves each cited extraction once instead of once per claim row. On a synthetic ledger of 2183 rows and 1000 living documents, `graph` drops from 14 s to 4 s. No new flag.
|
|
15
|
+
|
|
5
16
|
## [1.39.0] - 2026-10-13
|
|
6
17
|
|
|
7
18
|
### Changed
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antoneeo/agentic-sdlc-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.0",
|
|
4
4
|
"description": "Documentation-First SDLC protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, installed support files and optional devPNT integration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
package/scripts/init.js
CHANGED
|
@@ -203,16 +203,16 @@ in the skill's \`routing.md\`: it decides which lens's method and validation rul
|
|
|
203
203
|
that unit. L1 never reaches it. Never refer to a document whose meaning differs by lens
|
|
204
204
|
("threat model", "vision", \`principles.md\`, \`handoff.md\`) by its bare name.
|
|
205
205
|
|
|
206
|
-
Select the owning lens by the unit's purpose, then apply only its risk scale.
|
|
207
|
-
The project's always-on pointer stays intact: consult the selected skill rather than
|
|
208
|
-
merging multiple mandatory ladders. All lenses already carry shared project memory
|
|
209
|
-
(\`memory.md\`); another installation is not needed for cross-domain recall.
|
|
206
|
+
Select the owning lens by the unit's purpose, then apply only its risk scale.
|
|
207
|
+
The project's always-on pointer stays intact: consult the selected skill rather than
|
|
208
|
+
merging multiple mandatory ladders. All lenses already carry shared project memory
|
|
209
|
+
(\`memory.md\`); another installation is not needed for cross-domain recall.
|
|
210
210
|
This file is NOT auto-loaded by any client: it is a note for you, deliberately additive.
|
|
211
|
-
Delete this note once the routing pointer is integrated.
|
|
211
|
+
Delete this note once the routing pointer is integrated.
|
|
212
212
|
`, 'multi-lens routing note');
|
|
213
213
|
if (wrote && protocolPreexisting) {
|
|
214
214
|
console.log(' ⚠️ A protocol pointer already existed and was NOT overwritten.');
|
|
215
|
-
console.log(' Integrate the routing pointer from AGENTIC_MULTI_LENS.md; keep one risk scale per unit.');
|
|
215
|
+
console.log(' Integrate the routing pointer from AGENTIC_MULTI_LENS.md; keep one risk scale per unit.');
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -14,17 +14,17 @@ python "<skill_dir>/scripts/sdlc_check.py" check
|
|
|
14
14
|
|
|
15
15
|
## 2. Check in CI (recommended for teams)
|
|
16
16
|
|
|
17
|
-
Copy all three validator files into the repository: `scripts/sdlc_check.py`, `scripts/sdlc_core.py`, and `scripts/knowledge.py`, keeping them side by side. Then add to the pipeline:
|
|
17
|
+
Copy all three validator files into the repository: `scripts/sdlc_check.py`, `scripts/sdlc_core.py`, and `scripts/knowledge.py`, keeping them side by side. Then add to the pipeline:
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
python tools/sdlc_check.py validate --strict
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
The validator ships as **three files**: the domain entry point, `sdlc_core.py` and `knowledge.py`. Keep all three together. The core alone still runs but omits shared-memory/KB checks and domain-specific overlays; it is not an equivalent CI gate. `check` includes the knowledge surface when present.
|
|
23
|
+
The validator ships as **three files**: the domain entry point, `sdlc_core.py` and `knowledge.py`. Keep all three together. The core alone still runs but omits shared-memory/KB checks and domain-specific overlays; it is not an equivalent CI gate. `check` includes the knowledge surface when present.
|
|
24
24
|
|
|
25
25
|
Effect: an unregenerated index, invalid frontmatter, a missing security section or incoherent states **block the pipeline** instead of relying on the agent's memory. `--strict` also fails on warnings and on a missing `ai_docs/`, so a wrong working directory cannot produce a green pipeline. This works because documents travel in the same PR as the code (Phase 5 rule).
|
|
26
26
|
|
|
27
|
-
Note: the copy in the repo is the authoritative one for CI; update it when you update the skill — all three files, together.
|
|
27
|
+
Note: the copy in the repo is the authoritative one for CI; update it when you update the skill — all three files, together.
|
|
28
28
|
|
|
29
29
|
**Projects whose docs root is not `ai_docs/`.** Pass `--docs-dir <name>` (e.g. `--docs-dir mkt_docs`) on any subcommand, or set `AGENTIC_SDLC_DOCS_DIR`. Without either, the validator walks up from the working directory and takes the nearest root it recognizes. If it finds two side by side — the shape of a half-finished migration — it refuses and names both rather than validating half a project and printing a verdict. `ai_docs/` remains the default and the recommended root: the parameter exists so a legacy tree can be read and migrated, not so a second one can be kept.
|
|
30
30
|
|
|
@@ -155,7 +155,7 @@ Gemini CLI — wire the same command into its startup-hook mechanism if present;
|
|
|
155
155
|
**Usage notes:**
|
|
156
156
|
- The hook assumes the working directory is the project root (standard Claude Code hook behavior); it also accepts `--root <path>`.
|
|
157
157
|
- **Hybrid/devPNT projects**: add `--hybrid` — the hook then appends a one-line pointer to run `devpnt_mcp_get_bootstrap` for the Master Plan / Knowledge Layer and does not replicate them; the filesystem orientation (router + handoff + README) still emits.
|
|
158
|
-
- Like the CI gate (§2), if you copied the validator into the repo, the hook references that copy — keep all three files current when you update the skill.
|
|
158
|
+
- Like the CI gate (§2), if you copied the validator into the repo, the hook references that copy — keep all three files current when you update the skill.
|
|
159
159
|
|
|
160
160
|
### Per-turn reminder: `remind` (UserPromptSubmit -- wired by default)
|
|
161
161
|
|
|
@@ -215,7 +215,7 @@ It aggregates the test files (`test_plan.py` + `test_session_start.py` + `test_s
|
|
|
215
215
|
|
|
216
216
|
**Optional CI** (same shape as §2, not mandatory): add a `run:` step invoking the `unittest discover` command above.
|
|
217
217
|
|
|
218
|
-
**T10 note:** if you copied the validator (all three files) and the `test_*.py` battery into the repo for CI, that copy is authoritative — keep it current when you update the skill.
|
|
218
|
+
**T10 note:** if you copied the validator (all three files) and the `test_*.py` battery into the repo for CI, that copy is authoritative — keep it current when you update the skill.
|
|
219
219
|
|
|
220
220
|
## `benefit` is a report, never a CI gate
|
|
221
221
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agentic-sdlc
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.41.0
|
|
4
4
|
description: Documentation-First SDLC protocol with risk-proportional triage, Vision as a guide, a complete Standalone mode and optional symbiosis with devPNT. Use for features, significant bugs, refactors, audits and documented maintenance.
|
|
5
5
|
author: Antonio Pinto (https://github.com/Antoneeo)
|
|
6
6
|
copyright: (c) 2026 Antonio Pinto
|
|
@@ -90,7 +90,7 @@ Triage decides IF documentation is due; this table decides WHICH document each e
|
|
|
90
90
|
| `solutions/MERGE_REVIEW_[feature].md` | Every L3, after the closure review: the integrator's brief — what to review (`sdlc_check.py review-map`), what was verified by which gate, what only a human can judge (`templates.md`). The PR description links it. | 5 |
|
|
91
91
|
| `vision/features/VISION_[feature].md` | Feature known multi-milestone at analysis time, OR the retroactive trigger: you are about to create the SECOND `ANALYSIS_*` on the same theme — extract the shared feature vision first, then let both analyses reference it. | 3 |
|
|
92
92
|
| `audit/handoff.md` (workstream registry) | **Never by hand — generated by `sdlc_check.py index` from the `HANDOFF_*.md` sources**, and `validate` errors when the two disagree. Regenerate at every L3 closure and at session end; the `Date:` header is derived, so no writer touches it. It is an inventory for lookup, not a work board: no assignment, no due dates, no ordering, no holder. | 5 / session end |
|
|
93
|
-
| `audit/HANDOFF_[feature].md` | **One per OPEN workstream, with or without volatile state** — it is the authored home of that workstream's registry row (frontmatter `workstream`/`level`/`branch`/`status`/`since`/`next`/`details`/`updated`), so no file means no row. Also carries the resume logistics; **the ANALYSIS Diary keeps the durable narrative (DRY)**, and this file is DELETED at closure — deleting it *is* removing the row. | 4 / 5 / session end |
|
|
93
|
+
| `audit/HANDOFF_[feature].md` | **One per OPEN workstream, with or without volatile state** — it is the authored home of that workstream's registry row (frontmatter `workstream`/`level`/`branch`/`status`/`since`/`next`/`details`/`updated`), so no file means no row. Its `next:` is the next concrete step in one sentence; what happened, and the state of the work, go to the Diary as a dated entry. Also carries the resume logistics; **the ANALYSIS Diary keeps the durable narrative (DRY)**, and this file is DELETED at closure — deleting it *is* removing the row. | 4 / 5 / session end |
|
|
94
94
|
| `audit/project_notes.md` | A note true for the whole project rather than for one workstream (release pending, environment quirk). Appended verbatim to the generated registry; it exists so regenerating cannot destroy notes that belong to no workstream. | 5 / session end |
|
|
95
95
|
| `audit/handoff.md` — converting an existing project (hand-written or pre-1.17 narrative) | **Lazily, at the first write — and then ALL AT ONCE.** Converting one row at a time is the state that loses the others, so `index` refuses to write while anything in the file is unaccounted for and names it. Until the first source file exists nothing generates, nothing errors, and the file is read verbatim exactly as today. | 5 / session end |
|
|
96
96
|
| `audit/audit_plan.md` (Standalone) | Bootstrap, and whenever a mapped area changes state (`sdlc_check.py mark` records the reference — git hash, else UTC timestamp). | 1 |
|
|
@@ -204,7 +204,7 @@ Hybrid L3:
|
|
|
204
204
|
### 4. Development and Testing
|
|
205
205
|
|
|
206
206
|
- Implement only after the documentation gate required by the level. **Flip the ANALYSIS frontmatter `PLANNED` → `IN_PROGRESS` when implementation starts** — that flip is what `gate` and the handoff registry key on, and nothing else performs it.
|
|
207
|
-
- Isolate the work: run an L3 change on its own branch. In Hybrid, prefer a git worktree from the start — a running devPNT server locks `.devpnt/*.db` and blocks in-place branch switches/merges in the primary worktree.
|
|
207
|
+
- Isolate the work: run an L3 change on its own branch. In Hybrid, prefer a git worktree from the start — a running devPNT server locks `.devpnt/*.db` and blocks in-place branch switches/merges in the primary worktree. A session that may run while another session uses the same checkout works in its **own git worktree**, and never switches branch, stashes, resets or runs `index` in a checkout another session may be using. Parallel work also shares ids: choose a new unit's id **after fetching**, as the first id free on every branch and tag the checkout can see (`check` names a collision while the unit is open).
|
|
208
208
|
- Modify surgically, consistently with the plan.
|
|
209
209
|
- Implementation work follows the TDD discipline in `tdd.md` (RED/GREEN/REFACTOR — the L2/L3 default; record the reason when it does not apply).
|
|
210
210
|
- Before implementing (L2/L3; L1 exempt), **consult the guide router** — the verdict already declared with the triage level (`## Operative Guides`). Re-run the lookup here only if the work has moved to a topic the first one did not cover, and say so if the verdict changes.
|
|
@@ -34,11 +34,11 @@ proof that it caused this incident.
|
|
|
34
34
|
Do not repeat an invalidated hypothesis unless new evidence reopens it.
|
|
35
35
|
|
|
36
36
|
3. **Test and trace the mechanism.** Reproduce and isolate the smallest faithful
|
|
37
|
-
failing case when feasible; bisect inputs, paths or changes when informative.
|
|
38
|
-
Skip a reproduction already known to omit the incident's causal conditions;
|
|
39
|
-
choose a faithful alternative or disclose the gap. Stop diagnostic checks when
|
|
40
|
-
further results cannot change the causal or fix decision; this does not replace
|
|
41
|
-
the regression and recovery verification in step 5.
|
|
37
|
+
failing case when feasible; bisect inputs, paths or changes when informative.
|
|
38
|
+
Skip a reproduction already known to omit the incident's causal conditions;
|
|
39
|
+
choose a faithful alternative or disclose the gap. Stop diagnostic checks when
|
|
40
|
+
further results cannot change the causal or fix decision; this does not replace
|
|
41
|
+
the regression and recovery verification in step 5.
|
|
42
42
|
If the incident is intermittent or inaccessible, use captured traces and
|
|
43
43
|
targeted diagnostic observations instead of declaring investigation impossible.
|
|
44
44
|
Add minimal instrumentation when evidence is insufficient. Prefer read-only
|
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
577
|
-
elif b >
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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,
|
|
1187
|
-
if rel != topic_rel and
|
|
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);
|
|
1909
|
-
# dates
|
|
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
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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:
|