@antoneeo/mkt-agentic-sdlc-skill 0.17.0 → 0.19.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/mkt-agentic-sdlc/ENFORCEMENT.md +5 -5
- package/skills/mkt-agentic-sdlc/SKILL.md +3 -3
- package/skills/mkt-agentic-sdlc/scripts/knowledge.py +88 -34
- package/skills/mkt-agentic-sdlc/scripts/mkt_check.py +9 -0
- package/skills/mkt-agentic-sdlc/scripts/sdlc_core.py +358 -6
- package/skills/mkt-agentic-sdlc/templates.md +12 -1
- package/skills/mkt-agentic-sdlc/vision.md +245 -245
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.19.0] - 2026-10-17
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- F-073 (shared validator and doctrine): one written form for a dated entry, `**YYYY-MM-DD — label.**`, is taught for spike outcomes and project notes that record an event, and shared readers let a chronology list those events the same way the validator reads them. Two advisories reach marketing, and neither fails a build, not even `--strict`: a HANDOFF `next:` longer than 240 characters on a `level: L3` file updated on or after 2026-10-18 (`next:` now holds only the next step), and, in `check`, an open L3 unit whose id another branch or tag uses for a different analysis. That id check identifies a unit by its ANALYSIS file; a marketing request record names none, so an engagement without an ANALYSIS is not checked yet. The doctrine now says to choose a new id after fetching, and that a session sharing a checkout with another session works in its own git worktree. Advisories print as `[advisory]` lines and never count toward the exit code.
|
|
7
|
+
|
|
8
|
+
## [0.18.0] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- 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.
|
|
12
|
+
- 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.
|
|
13
|
+
|
|
3
14
|
## [0.17.0] - 2026-10-13
|
|
4
15
|
|
|
5
16
|
### Changed
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mkt-agentic-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Evidence-First marketing planning protocol with engagement triage, SOSTAC workflow, evidence ledger and optional devPNT integration.",
|
|
5
5
|
"author": "Antonio Pinto (https://github.com/Antoneeo)"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antoneeo/mkt-agentic-sdlc-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Evidence-First marketing planning protocol for Claude Code, Gemini CLI, Google Antigravity and Codex: engagement triage, SOSTAC workflow, evidence ledger, mechanical validator and optional devPNT integration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
package/scripts/init.js
CHANGED
|
@@ -194,16 +194,16 @@ in the skill's \`routing.md\`: it decides which lens's method and validation rul
|
|
|
194
194
|
that unit. L1 never reaches it. Never refer to a document whose meaning differs by lens
|
|
195
195
|
("threat model", "vision", \`principles.md\`, \`handoff.md\`) by its bare name.
|
|
196
196
|
|
|
197
|
-
Select the owning lens by the unit's purpose, then apply only its risk scale.
|
|
198
|
-
The project's always-on pointer stays intact: consult the selected skill rather than
|
|
199
|
-
merging multiple mandatory ladders. All lenses already carry shared project memory
|
|
200
|
-
(\`memory.md\`); another installation is not needed for cross-domain recall.
|
|
197
|
+
Select the owning lens by the unit's purpose, then apply only its risk scale.
|
|
198
|
+
The project's always-on pointer stays intact: consult the selected skill rather than
|
|
199
|
+
merging multiple mandatory ladders. All lenses already carry shared project memory
|
|
200
|
+
(\`memory.md\`); another installation is not needed for cross-domain recall.
|
|
201
201
|
This file is NOT auto-loaded by any client: it is a note for you, deliberately additive.
|
|
202
|
-
Delete this note once the routing pointer is integrated.
|
|
202
|
+
Delete this note once the routing pointer is integrated.
|
|
203
203
|
`, 'multi-lens routing note');
|
|
204
204
|
if (wrote && protocolPreexisting) {
|
|
205
205
|
console.log(' ⚠️ A protocol pointer already existed and was NOT overwritten.');
|
|
206
|
-
console.log(' Integrate the routing pointer from AGENTIC_MULTI_LENS.md; keep one risk scale per unit.');
|
|
206
|
+
console.log(' Integrate the routing pointer from AGENTIC_MULTI_LENS.md; keep one risk scale per unit.');
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -14,17 +14,17 @@ python "<skill_dir>/scripts/mkt_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/mkt_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/mkt_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/mkt_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: mkt-agentic-sdlc
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.19.0
|
|
4
4
|
description: Evidence-First marketing planning protocol with engagement triage, SOSTAC workflow, evidence ledger, adversarial CMO review, a complete Standalone mode and optional symbiosis with devPNT. Use for marketing plans, go-to-market strategy, campaign planning and market research.
|
|
5
5
|
author: Antonio Pinto (https://github.com/Antoneeo)
|
|
6
6
|
copyright: (c) 2026 Antonio Pinto
|
|
@@ -79,7 +79,7 @@ One event, one destination: when the trigger fires and the document does not exi
|
|
|
79
79
|
|---|---|---|
|
|
80
80
|
| `audit/requests/REQUEST_[engagement].md` | Every E3, the moment the level is declared — before Wave 1 and the Vision Gate: R1 quotes the client's opening words verbatim; every later statement of theirs that settles something (answer, approval, budget, ruling) is appended before acting on it. Append-only, never deleted; artifacts and owner-given `FACT` rows cite its entries (`templates.md`). | 1 → 9 |
|
|
81
81
|
| `audit/handoff.md` (workstream registry) | **Never by hand — generated by `mkt_check.py index` from the `HANDOFF_*.md` sources**, and `validate` errors when the two disagree. Regenerate at every closure and at session end; the `Date:` header is derived, so no writer touches it. Inventory for lookup, never a work board. | 9 / session end |
|
|
82
|
-
| `audit/HANDOFF_[engagement].md` | **One per OPEN engagement, with or without volatile state** — it is the authored home of that engagement's registry row (frontmatter `workstream`/`level`/`branch`/`status`/`since`/`next`/`details`/`updated`), so no file means no row. DELETED at closure — deleting it *is* removing the row. | 9 / session end |
|
|
82
|
+
| `audit/HANDOFF_[engagement].md` | **One per OPEN engagement, with or without volatile state** — it is the authored home of that engagement'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. DELETED at closure — deleting it *is* removing the row. | 9 / session end |
|
|
83
83
|
| `audit/project_notes.md` | A note true for the whole engagement portfolio rather than one engagement. Appended verbatim to the generated registry, so regenerating cannot destroy it. | 9 / session end |
|
|
84
84
|
| `audit/handoff.md` — converting an existing project | **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. | 9 / session end |
|
|
85
85
|
| `audit/HANDOFF_[engagement].md` | Session ends with that engagement unfinished AND there is volatile resume state. Ephemeral, deleted at closure. | 8 / 9 / session end |
|
|
@@ -167,7 +167,7 @@ Deliverable language: ask for (or infer) the target market's language in Discove
|
|
|
167
167
|
- `mkt_check.py budget && mkt_check.py funnel && mkt_check.py trace` must pass. A tactic that serves no objective is deleted or the objective set is revisited — never silently kept.
|
|
168
168
|
|
|
169
169
|
### 8. Action
|
|
170
|
-
- **Isolate the work (Branch/worktree hygiene).** Execution rewrites plan documents other people are reading: do it on a branch or a worktree, never directly on the shared plan.
|
|
170
|
+
- **Isolate the work (Branch/worktree hygiene).** Execution rewrites plan documents other people are reading: do it on a branch or a worktree, never directly on the shared plan. 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.
|
|
171
171
|
- **Consult (before acting)**: **consult the guide router** for a guide covering the task and read it first (the consult trigger, `guides.md` §0). A targeted description match, not a blanket read.
|
|
172
172
|
- **Opt-in subagent execution**: for an approved plan, the work MAY be executed via subagents per `dispatch.md`; default stays same-session. `dispatch.md` also carries the family-wide **delegation boundary** (what may be delegated at all, and what never may) — that section is not gated by the opt-in trigger and applies at any level, dispatch or no dispatch.
|
|
173
173
|
- Draft `ACTION_90D.md`: sequenced 90-day plan (what starts when, owner, dependency), first 2 weeks day-level, rest weekly.
|
|
@@ -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:
|
|
@@ -735,6 +735,15 @@ def main(argv=None):
|
|
|
735
735
|
code = max(code, sdlc_core.cmd_validate(root, strict=args.strict))
|
|
736
736
|
code = max(code, knowledge.kb_validate_surface(root / sdlc_core.docs_dir(),
|
|
737
737
|
full=args.command == "check"))
|
|
738
|
+
# F-073 advisories: Report has no advisory channel and --strict fails on
|
|
739
|
+
# any warning, so they print here, outside Report, never in `code`. On an
|
|
740
|
+
# ai_docs root cmd_validate above already printed A1/A2 -- once is the
|
|
741
|
+
# contract. A3 has no other path: this overlay never calls cmd_check.
|
|
742
|
+
notes = [] if sdlc_core.docs_dir() == "ai_docs" else sdlc_core.chronology_advisories(root)
|
|
743
|
+
if args.command == "check":
|
|
744
|
+
notes += sdlc_core.id_advisories(root)
|
|
745
|
+
for a in notes:
|
|
746
|
+
print(f"[advisory] {a}")
|
|
738
747
|
if code == 0:
|
|
739
748
|
print(f"[OK] {args.command} clean "
|
|
740
749
|
f"({len(rep.warnings)} warning(s))" if rep.warnings else f"[OK] {args.command} clean")
|