@antoneeo/mkt-agentic-sdlc-skill 0.26.0 → 0.27.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.27.0] - 2026-10-27
4
+
5
+ ### Added
6
+ - Shared knowledge layer (F-084, kb CHANGELOG): `inventory <path>` and orient's upper bound of never-examined project files reach this lens; `validate`/`check` refuse a malformed `audit/INVENTORY_exclusions.md` row and the new `DECLARED`/`DIVERGES` claim rules; the docs site renders them. `sdlc_core.skip_dir` is extracted from `iter_files`, behaviour unchanged.
7
+ - `constraints --unit <id>` and `constraints --file <path>` (shared core): the constraints an ANALYSIS states in extractable form, each with its line, ending with the sections they could not extract.
8
+ - `benefit` and the docs site recognize a REVIEW_LOG row with tier `post-merge` (written by the code lens: the owner's report of a wrong behaviour in a merged change): `benefit` keeps it out of every review figure, the docs site shows it as "defect reported after merge". `review.md`: a reviewer derives callers and behaviours before comparing with the constraint view.
9
+ - `guides.md`: a code guide may carry a `## Preservation constraints` section copied from a closed unit; it survives regeneration. Nothing else changes in this lens: `validate` adds no note.
10
+
11
+ ### Fixed
12
+ - Shared claim checks (`check`): a `RULING` row on a note that names a foreign origin (`imported_from:`) is refused — relabelling an imported row is not ratifying it; the message for a row superseded by an `IMPORTED` one names the ratification act (F-083).
13
+ - Guide-verdict check (`verdict --hook`, shared core): a level quoted inside a sentence (`Level: L1` in prose) no longer counts as a declaration: only a line that opens with the level word does, the rule the constraint recall already uses (level 1 alone, `L1` or `E1`; levels 2 and 3 with `router` on the same line). The check now judges the project, and its docs-root name, that holds the edited file, not the one around the session's working directory; after a `cd` out of the project it let every edit through. A router verdict follows the same rule: only a line that opens with `router` or declares the level with it counts, never `router: no match` quoted in a sentence; `Level: Spike · router: …` and markdown openers (`##`, `1.`, `•`) count. A quote that itself opens a line (blockquote, code fence) still counts. The protected-path gate (`gate --hook`, `gate --file`) also judges the edited file's project, falls back to the session's project when no docs root sits above the file, and stays silent instead of exiting with an error when two docs roots sit side by side. Its `--protected` prefixes are relative to the nearest docs root above the edited file: with nested docs roots (a monorepo package with its own `ai_docs/`, or a docs folder inside a protected path) rewrite them relative to that root, or the path is no longer protected.
14
+
3
15
  ## [0.26.0] - 2026-10-23
4
16
 
5
17
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mkt-agentic-sdlc",
3
- "version": "0.26.0",
3
+ "version": "0.27.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.26.0",
3
+ "version": "0.27.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",
@@ -54,15 +54,15 @@ Semantics: exit code 2 + message on stderr ⇒ the write is blocked and the mess
54
54
 
55
55
  **Usage warnings:**
56
56
  - The gate is deliberately coarse: applied to all of `src/` it would also block the legitimate L1/L2 tasks foreseen by the Triage. Use it **only on security-critical directories** (`--protected "src/auth;src/crypto"`), where "never without analysis" is the desired policy.
57
- - The paths in `--protected` are prefixes relative to the project root, separated by `;`.
57
+ - The paths in `--protected` are prefixes relative to the root of the project the hook judges (below), separated by `;`. With nested docs roots (a monorepo whose package has its own), that is the nearest one above the edited file: write the prefixes relative to it.
58
58
  - `ai_docs/`, `tests/` and `test/` are always excluded from blocking.
59
- - The hook assumes the working directory is the project root (standard behavior of Claude Code hooks).
59
+ - The hook judges the project, and the docs root, holding the edited file: the session's working directory can move with a `cd`, so it is never the reference. With no docs root above the file, it is the session's own project, as before. `--root <path>` overrides it; `gate --file` resolves the same way, and two docs roots side by side leave the gate silent.
60
60
  - **Hybrid/devPNT projects**: add `--hybrid` to the gate command. Governed designs live in the devPNT DB, so the gate also unlocks when an approved E-TDD shadow (`ai_docs/solutions/SHADOW_*tdd*.md`, exported before implementation — see `hybrid.md` §Ownership matrix) is present. Without the flag the gate would block legitimate governed work. The flag is deliberately explicit: never auto-detected.
61
61
 
62
62
  ### Verdict check: `verdict` (PreToolUse -- manual for this lens)
63
63
 
64
64
  `mkt_check.py verdict --hook` refuses an Edit, Write, MultiEdit or NotebookEdit when the
65
- session transcript shows no guide-router verdict (`router: …`, or a level-1 declaration)
65
+ session transcript shows no guide-router verdict (a line that opens with `router: …` or with the level, `Level: L2 · router: …`, `Level: Spike · router: …`; or one that opens with a level-1 declaration)
66
66
  or a guide the verdict names was never read; it is silent in projects without guides and
67
67
  whenever it cannot tell. The code package (`agentic-sdlc`) wires it machine-wide on
68
68
  Claude Code; this package does not. To wire it by hand, add a `PreToolUse` entry with
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mkt-agentic-sdlc
3
- version: 0.26.0
3
+ version: 0.27.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
@@ -12,7 +12,7 @@
12
12
  var D = null; // documents, loaded on first use
13
13
  var K = null; // knowledge base, loaded on first use
14
14
  var app = document.getElementById("app");
15
- var ORIGINS = ["given", "elicited", "derived", "ruling", "imported"];
15
+ var ORIGINS = ["given", "elicited", "derived", "ruling", "imported", "declared"];
16
16
 
17
17
  // ------------------------------------------------------------- helpers
18
18
 
@@ -483,6 +483,7 @@
483
483
  if (f.statement) marks.push(el("span", { "class": "tag statement", title: t("statement_notice"), text: t("statement_mark") }));
484
484
  if (f.state === "superseded") marks.push(el("span", { "class": "tag replaced", title: t("state_superseded_why"), text: t("state_superseded") }));
485
485
  if (f.state === "contested") marks.push(el("span", { "class": "tag conflict", title: t("state_contested_why"), text: t("state_contested") }));
486
+ if (f.state === "diverges") marks.push(el("span", { "class": "tag conflict", title: t("state_diverges_why"), text: t("state_diverges") }));
486
487
  if (opts && opts.topic) marks.push(link("#/topic/" + encodeURIComponent(f.topic), f.topic, "chip"));
487
488
  return el("li", { "class": "fact" + (f.state === "superseded" ? " replaced" : ""), onclick: function () { location.hash = "#/fact/" + encodeURIComponent(id); } },
488
489
  el("span", { "class": "dot bg-" + oc }),
@@ -597,6 +598,11 @@
597
598
  el("p", { "class": "why-line", text: t("state_contested_why") }),
598
599
  el("ul", { "class": "facts" }, (f.with || []).map(function (s) { return factLine(s, { topic: true }); })));
599
600
  }
601
+ if (f.state === "diverges") {
602
+ parts.push(el("h3", null, el("span", { "class": "tag conflict", text: t("state_diverges") }), " ", t("fact_diverges_from")),
603
+ el("p", { "class": "why-line", text: t("state_diverges_why") }),
604
+ el("ul", { "class": "facts" }, (f.diverges_from || []).map(function (s) { return factLine(s, { topic: true }); })));
605
+ }
600
606
  if (f.grounds) {
601
607
  parts.push(el("h3", { text: t("fact_rests_on") }),
602
608
  el("p", { "class": "why-line", text: t("ground_class_" + f.ground_class) }),
@@ -56,7 +56,9 @@ window.DOCS_SITE_STRINGS = {
56
56
  event_request: "request record",
57
57
  event_note: "note",
58
58
  event_source: "source document received",
59
+ event_sent: "document sent",
59
60
  event_review: "review",
61
+ event_defect: "defect reported after merge",
60
62
  event_decision: "decision record",
61
63
  event_project_note: "project note",
62
64
  event_spike: "exploration outcome",
@@ -231,6 +233,8 @@ window.DOCS_SITE_STRINGS = {
231
233
  origin_ruling_why: "Taken to settle a conflict.",
232
234
  origin_imported: "from another knowledge base",
233
235
  origin_imported_why: "Brought in from another knowledge base, where its own sources are.",
236
+ origin_declared: "declared by this project",
237
+ origin_declared_why: "What this project told a recipient, on the day it was sent: a statement of ours, not a fact about the product.",
234
238
  origin_other: "origin as written: {v}",
235
239
  origin_other_why: "An origin this site does not know.",
236
240
 
@@ -239,6 +243,8 @@ window.DOCS_SITE_STRINGS = {
239
243
  state_superseded_why: "A newer fact takes its place.",
240
244
  state_contested: "in conflict",
241
245
  state_contested_why: "Facts that disagree, not yet settled.",
246
+ state_diverges: "diverges",
247
+ state_diverges_why: "What we declared contradicts what the project decided. It is corrected by a new send, or the decision changes.",
242
248
 
243
249
  statement_mark: "statement",
244
250
  statement_prefix: "[statement] {text}",
@@ -254,6 +260,7 @@ window.DOCS_SITE_STRINGS = {
254
260
  fact_replaced_by: "Replaced by",
255
261
  fact_current_at_end: "Current fact at the end of each chain",
256
262
  fact_in_conflict_with: "In conflict with",
263
+ fact_diverges_from: "Diverges from",
257
264
  fact_rests_on: "What it rests on",
258
265
  fact_untraced: "This part could not be traced.",
259
266
  fact_sources: "Sources",
@@ -266,6 +273,7 @@ window.DOCS_SITE_STRINGS = {
266
273
  fact_unknown: "This fact is not in this knowledge base.",
267
274
 
268
275
  ground_document: "a document",
276
+ ground_declared: "a document this project sent",
269
277
  ground_statement: "a person's statement",
270
278
  ground_decision: "a decision",
271
279
  ground_kb: "another knowledge base",
@@ -273,6 +281,7 @@ window.DOCS_SITE_STRINGS = {
273
281
  ground_class_statement: "Rests, at least in part, on a person's statement.",
274
282
  ground_class_untraceable: "Part of what it rests on could not be traced.",
275
283
  ground_class_imported: "Rests on another knowledge base.",
284
+ ground_class_declared: "Rests on our own declaration to a recipient, not on evidence.",
276
285
  ground_class_documents: "Rests on documents or decisions only.",
277
286
 
278
287
  chain_loop: "The chain of replacements loops back to {id}: it stops here.",
@@ -18,6 +18,7 @@
18
18
  --o-derived: #a99be0;
19
19
  --o-ruling: #7fa7e6;
20
20
  --o-imported: #9cc96b;
21
+ --o-declared: #d98fb0;
21
22
  --conflict: #e0605a;
22
23
  --display: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Charter, Georgia, serif;
23
24
  --body: "Segoe UI Variable Text", "Avenir Next", Avenir, "Gill Sans Nova", "Gill Sans", "Trebuchet MS", sans-serif;
@@ -179,6 +180,7 @@ a.factid code { color: var(--link); }
179
180
  .o-derived { color: var(--o-derived); } .bg-derived { background: var(--o-derived); }
180
181
  .o-ruling { color: var(--o-ruling); } .bg-ruling { background: var(--o-ruling); }
181
182
  .o-imported { color: var(--o-imported); } .bg-imported { background: var(--o-imported); }
183
+ .o-declared { color: var(--o-declared); } .bg-declared { background: var(--o-declared); }
182
184
  .o-other { color: var(--paper-3); } .bg-other { background: var(--paper-3); }
183
185
 
184
186
  .panel {
@@ -262,6 +262,12 @@ in the snapshot, never to your assumption about what the code does.
262
262
  (source-anchored), not an elaborated version of it.
263
263
  - When in doubt about whether something is "supported" by the source, treat it
264
264
  as not covered rather than stretching the marker to fit.
265
+ - **Declared exception: `## Preservation constraints`.** A code guide may carry
266
+ this section, copied at an L3 closure from the ANALYSIS's table (code lens,
267
+ `SKILL.md` Phase 5). The section's marker cites each row it holds as
268
+ `[source: solutions/ANALYSIS_<name>.md#P<n>]` instead of a snapshot anchor: each
269
+ row records what the code must keep doing, as that unit enumerated and probed it,
270
+ not an excerpt of the code.
265
271
 
266
272
  ## 4. Ingestion bound (T9)
267
273
 
@@ -293,5 +299,7 @@ state it explicitly when handing off a newly created guide.
293
299
  closure (docs travel with the code); when you merely READ one, check it against
294
300
  the code before trusting it. Too large to absorb → mark it `SUPERSEDED` rather
295
301
  than leave a confident-wrong map badged CURRENT, which is worse than none.
302
+ - **A regenerated guide keeps `## Preservation constraints` unchanged**: the
303
+ section does not come from the snapshot (§3), so regeneration carries it over.
296
304
  - **Agent-global KB guides** use the same pipeline and validator via
297
305
  `--root ~/.agentic-sdlc`; freshness via the same `stale` engine.
@@ -187,7 +187,10 @@ can block forever gets removed. **Log one row per completed review, PASS or FAIL
187
187
  logging only passes would erase exactly that evidence. The row goes in
188
188
  `ai_docs/audit/reviews/REVIEW_LOG.md` (create it if absent — `templates.md`):
189
189
  `| date | doc_key | tier | model | reviewer | findings_raised | findings_real | verdict | revise_rounds |`,
190
- with `tier` = `design` or `closure` in Standalone. **`model` records the capability
190
+ with `tier` = `design` or `closure` in Standalone. In the code lens, a wrong behaviour
191
+ the owner reports in a merged L3 change is one row too, tier `post-merge`, `model`
192
+ `none (owner report)`, reviewer `owner`, the owner quoted in the notes: a defect
193
+ record, not a review, so `benefit` keeps it out of every review figure. **`model` records the capability
191
194
  tier that actually ran** — `deep`, `light`, `economy`, `single (client exposes no
192
195
  choice)`, `below floor: <reason>`, or `none (not due)` on a NOT DUE row — never a provider name in doctrine (a project
193
196
  may of course write its own model names in its own log). Without it the floor above
@@ -285,6 +288,11 @@ The binding inputs, by review type:
285
288
  clause reads both), and the **probe harness** (`ai_docs/solutions/harness_[feature]/`).
286
289
  Hand these *in addition to* the design artifact — the reviewer checks the artifact
287
290
  **against** them, not only for internal consistency.
291
+ - **The constraint view comes last.** Where the ANALYSIS carries ids, the reviewer
292
+ derives the callers and behaviours first, then compares with
293
+ `sdlc_check.py constraints --unit <id>`: a constraint the design states and the
294
+ diff breaks is a finding. The view never replaces the ANALYSIS or the diff in the
295
+ request.
288
296
  - **For a closure review, the diff and the test evidence**: the run output that
289
297
  backs every completion claim, dated after the final relevant change.
290
298
  - **For a design or closure review of an L3/E3 unit, the request record**
@@ -615,6 +615,11 @@ class Grounds:
615
615
  return [{"kind": "untraceable", "path": rel}]
616
616
  seen = seen | {rel}
617
617
  if rel.startswith("corpus/given/"):
618
+ # F-084: a document this project SENT is our own declaration, not
619
+ # ground for a fact about the product.
620
+ side = self._meta(rel + ".meta.md")
621
+ if _v(side, "provenance").upper() == "DECLARED":
622
+ return [{"kind": "declared", "path": rel}]
618
623
  return [{"kind": "document", "path": rel}]
619
624
  if not _inside(self.docs, rel) or not (self.docs / rel).is_file():
620
625
  return [{"kind": "untraceable", "path": rel}]
@@ -643,6 +648,8 @@ def ground_class(leaves):
643
648
  return "statement"
644
649
  if "untraceable" in kinds:
645
650
  return "untraceable"
651
+ if "declared" in kinds:
652
+ return "declared"
646
653
  if "kb" in kinds:
647
654
  return "imported"
648
655
  return "documents"
@@ -707,12 +714,15 @@ def build_kb(docs):
707
714
  kind, rest = _kind(r)
708
715
  f = {"text": r["claim"], "valid": r["valid"], "qty": r["qty"], "about": r["about"],
709
716
  "origin": (r["prov"] or "").strip().lower(), "topic": slug,
710
- "state": {"SUPERSEDED": "superseded", "CONTESTED": "contested"}.get(kind, "ok")}
717
+ "state": {"SUPERSEDED": "superseded", "CONTESTED": "contested",
718
+ "DIVERGES": "diverges"}.get(kind, "ok")}
711
719
  if kind == "SUPERSEDED":
712
720
  f["successors"] = _ids(rest)
713
721
  f["chain_ends"] = chain_ends(r["id"], by_id)
714
722
  if kind == "CONTESTED":
715
723
  f["with"] = _ids(rest)
724
+ if kind == "DIVERGES":
725
+ f["diverges_from"] = _ids(rest)
716
726
  srcs = []
717
727
  for src in r["source"].split(";"):
718
728
  src = src.strip()
@@ -1246,7 +1256,9 @@ def chronology(docs, pages):
1246
1256
  text = _text(docs / rel) if (docs / rel).is_file() else None
1247
1257
  if text is not None:
1248
1258
  for row in sdlc_core.parse_review_log(text)[0]:
1249
- add(row["date"], "review", "%s — %s: %s" % (
1259
+ # F-085: a `post-merge` row is the owner's defect report, not a review
1260
+ kind = "defect" if row["tier"].lower().startswith("post-merge") else "review"
1261
+ add(row["date"], kind, "%s — %s: %s" % (
1250
1262
  row["doc_key"], row["tier"], row["verdict"]), rel)
1251
1263
  for p in sorted(docs.glob("architecture/ADR_*.md")):
1252
1264
  rel = "architecture/" + p.name
@@ -1269,7 +1281,9 @@ def chronology(docs, pages):
1269
1281
  continue
1270
1282
  if kind == "source":
1271
1283
  name = p.name[:-len(suffix)]
1272
- add(date, kind, name, rel, source=name)
1284
+ # F-084: a DECLARED sidecar's date is the day it was SENT
1285
+ sent = _v(fm, "provenance").upper() == "DECLARED"
1286
+ add(date, "sent" if sent else kind, name, rel, source=name)
1273
1287
  else:
1274
1288
  add(date, kind, title_of(rel, text), rel)
1275
1289
  # Most recent first. Within one day a document's later entry is the more recent