@antoneeo/mkt-agentic-sdlc-skill 0.21.1 → 0.26.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 +54 -0
- package/README.md +3 -3
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/mkt-agentic-sdlc/ENFORCEMENT.md +20 -8
- package/skills/mkt-agentic-sdlc/SKILL.md +3 -2
- package/skills/mkt-agentic-sdlc/docs_site/app.js +9 -3
- package/skills/mkt-agentic-sdlc/guides.md +7 -1
- package/skills/mkt-agentic-sdlc/memory.md +1 -1
- package/skills/mkt-agentic-sdlc/review.md +28 -3
- package/skills/mkt-agentic-sdlc/routing.md +4 -0
- package/skills/mkt-agentic-sdlc/scripts/docs_site.py +94 -8
- package/skills/mkt-agentic-sdlc/scripts/knowledge.py +519 -17
- package/skills/mkt-agentic-sdlc/scripts/mkt_check.py +1 -1
- package/skills/mkt-agentic-sdlc/scripts/sdlc_core.py +581 -70
- package/skills/mkt-agentic-sdlc/templates.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.26.0] - 2026-10-23
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- `guides.md`: a document snapshot is verbatim except for credentials and personal or confidential data the guide does not need, which become `[REDACTED: <kind>]` before the snapshot is written; the guide lists the kinds in `redacted_at_intake:`.
|
|
7
|
+
- The session-start orientation (`orient`) now fits under Claude Code's 10,000-character hook limit, even in a project with many documents. Above that limit Claude Code saves the output to a file and shows the agent only the first 2,000 characters, so the guide router never reached the agent. The output is now at most 9,500 characters, including the knowledge layer's lines, and in this order: the triage line, the guide router (one line per guide), the workstream registry (one line per workstream, then the project-wide notes), then the reading guide (README). The manifest (INDEX) and the full files are listed as pointers. A section that does not fit is cut at a line, with a pointer to its file.
|
|
8
|
+
- The per-turn `remind` line also tells the agent, above L1 and when sibling lenses are installed, to choose the lens with `routing.md` and declare it. The line's limit rises from 500 to 600 characters (`vision/rulings.md` r32).
|
|
9
|
+
- `docs_site.py` adds `.docs-site/` to the project's `.gitignore` on its first run in a git repository, so the site's copy of the documents is not committed. It used to print a note only. An entry already in the file is never changed; when git still does not ignore the folder, the note is printed as before.
|
|
10
|
+
- `SKILL.md` names the docs site in the words users use ("docs site", "minisito", "documentation site", "project site"), so a request for it runs `docs_site.py` instead of building a page by hand.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `check` refuses, in every lens, a line shaped like a credential (a password, token or key with a real value, a private-key header, a password in a URL, a bearer token) in a `corpus/given/` file or in the snapshot of a `source_kind: document` guide. The message names the file, the lines and the kinds, never the value. A human clears a false positive with `credential_scan: reviewed — <reason>` (in the sidecar, or in the guide). A project whose stored sources already hold such a line fails `check` until each is redacted or reviewed (`vision/rulings.md` r35a).
|
|
14
|
+
- `adopt`: a dry run listing the document guides that rest on `.sources/` snapshots and what moving each into `corpus/given/` involves. It writes nothing.
|
|
15
|
+
- `check` warns when the same text is stored both as a guide snapshot and as a corpus artifact.
|
|
16
|
+
- `check` also warns on a `corpus/given/` file with no sidecar, accepts a withdrawn artifact's tombstone sidecar (no bytes, no digest), and, when an `original_path:` is a URI, names `original_ref:` for non-local identifiers. A password inside a URL in that warning is masked.
|
|
17
|
+
- Shared claim checks (`check`): a claim's `about` cell may hold several relations separated by `; `, and one reserved predicate, `not-yet-honored-by -> ANALYSIS_<name>`, marks a specification claim the code does not yet honor. `check` refuses the marker when that analysis is missing from `solutions/` or cites neither the claim id nor the id of a row the claim succeeds. `export` drops the markers and says so in one line. Nothing lists or counts claims by the marker (`vision/rulings.md` r33). `memory.md`: a marked claim is cited as not yet true of the code, naming its analysis.
|
|
18
|
+
- Shared claim checks (`check`): the `not-yet-honored-by` marker now works in Hybrid projects (devPNT present). It may name `SHADOW_<doc_key>`; `check` reads the newest `solutions/SHADOW_<doc_key>_vX.Y.md` (versions compared number by number), which must exist and cite the claim id or a predecessor's. A missing shadow is an error that says to export it, and names a file of that key whose name lacks the `_vX.Y` suffix. `check` behaves the same with or without `--hybrid`; export drops both target forms (`vision/rulings.md` r36). New shared helper `sdlc_core.newest_shadow`. `memory.md`: a marked claim is cited naming its target.
|
|
19
|
+
- `verdict --hook` (PreToolUse): refuses an edit when the session declared no guide-router verdict, or never read a guide its verdict names; silent in projects without guides and whenever it cannot tell. Not wired by this package (`ENFORCEMENT.md` §3).
|
|
20
|
+
- `orient` adds a note when one client runs it from two settings entries, which means each session gets the orientation twice. Claude Code and Codex are counted separately, an identical command counts once, and an entry whose validator is gone is not counted.
|
|
21
|
+
- `orient` adds a line naming the sibling lenses installed beside the skill or in the user's Claude skills directory, and says to choose the lens with `routing.md`. The family list is read from `routing.md` §0, as the installer does.
|
|
22
|
+
- `routing.md` §2 gains worked verdicts for four mixed requests: an interface spec written for another team, the requirements another team must build, a guide whose source spec changed, and code that diverges from a supplied spec.
|
|
23
|
+
- The shared validator adds an advisory for a code-lens (`domain: code`) L3 analysis, started on or after 2026-10-24, that lacks a filled `## Guide check` section. Analyses of this lens are not checked.
|
|
24
|
+
|
|
25
|
+
## [0.25.0] - 2026-10-19
|
|
26
|
+
|
|
27
|
+
Includes everything listed under [0.24.0], which was tagged but never published to npm.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `mark <paths> --note "<what was done>"` writes a dated entry `**YYYY-MM-DD — <what was done>.**` at the top of `audit/project_notes.md`. A release or work outside an open unit used to leave no dated event, because the date had to be copied there by hand. Without `--note`, and with no open unit, `mark` says so.
|
|
31
|
+
- `validate`: an advisory for a dated entry in `project_notes.md` whose bold does not pair; the chronology then showed the label running into the body.
|
|
32
|
+
- Docs site chronology: a Diary entry's label carries its unit id ("F-074 — closed"), so it reads on its own.
|
|
33
|
+
- `stale` no longer counts files inside the docs root (`ai_docs/`, `mkt_docs/`) as drift of any area, and `mark` refuses to record it: `validate` already checks the docs root, and every document edit used to leave `check` NOT CLEAN until a redundant re-mark.
|
|
34
|
+
|
|
35
|
+
## [0.24.0] - 2026-10-19
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- Docs site: `docs_site.py --zip <file>` also writes the site as one .zip to share; unzipped, `index.html` opens on any system. A zip made by hand with Windows PowerShell 5.1 `Compress-Archive` stores `data\core.js`, and on Linux or macOS the page then finds no data; `--zip` always writes `/` names. It refuses a target inside the docs root or the site folder. The `SKILL.md` line names it.
|
|
39
|
+
|
|
40
|
+
## [0.23.0] - 2026-10-19
|
|
41
|
+
|
|
42
|
+
Includes everything listed under [0.22.0], which was tagged but never published to npm.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- Docs site: each card in Open work and each feature in Features links its merge review and its end-to-end test script, when they exist. They were reachable only inside a feature's chain page; who merges looks in Open work while the work is open, who tests looks in Features after it closed.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- Docs site: Features are listed newest first by their declared start date; those without a start date come last.
|
|
49
|
+
|
|
50
|
+
## [0.22.0] - 2026-10-18
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
- F-071: a review weighs effort against what the owner asked for. In a design review, each outcome the owner named (the opening request, any entry widening scope, an approved north star) is a row of the conformance statement, saying where the design serves it and what share of the design goes to it; an outcome no section serves, or one served by a minor share while a secondary concern takes the bulk, is a finding. `review.md` §Reviewing.
|
|
54
|
+
- F-071: a review runs where something is left to judge. At level 2 the closure review is due only on a security-sensitive area the lens's Rule Zero names, or at the owner's request; otherwise it does not run and logs no row. After a design or closure PASS, a delta made only of version and release metadata, regenerated indexes and audit-mark records, tool-verified copies of a shared file, and CHANGELOG lines adding no claim is not reviewed: its own check runs and a `NOT DUE` row records it. README or overview prose, an E2E script, a rulings row, doctrine, behaviour, tests and any correction of a review finding are still reviewed. `review.md` §When a review is due.
|
|
55
|
+
- `sdlc_check.py benefit` counts `NOT DUE` rows apart, in no review or findings figure.
|
|
56
|
+
|
|
3
57
|
## [0.21.1] - 2026-10-18
|
|
4
58
|
|
|
5
59
|
### Fixed
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Turns an AI agent into a disciplined marketing strategist that produces a profes
|
|
|
27
27
|
3. **Adversarial CMO review** — an independent, fresh-context reviewer attacks the strategy before the user sees it (generic-positioning swap test, untraced claims, orphan tactics).
|
|
28
28
|
4. **Verbatim request record** — every E3 engagement starts `audit/requests/REQUEST_[engagement].md` before Wave 1: the client's own words and every later decision, append-only and redacted for secrets; owner-given FACT rows and the plan cite its entries, and reviews check client statements against it.
|
|
29
29
|
|
|
30
|
-
The **docs site** (`scripts/docs_site.py --root <project> [--open]`) writes a static, offline, read-only website of what the agent reads in the project — reading guide, Vision, rules, documents, open work with each next step, and, with a knowledge base, the topic map, each fact with its exact source passage, and the source documents. It is written outside the docs root (`mkt_docs/` or `ai_docs/`); nothing is ranked or recommended.
|
|
30
|
+
The **docs site** (`scripts/docs_site.py --root <project> [--open] [--zip <file>]`) writes a static, offline, read-only website of what the agent reads in the project — reading guide, Vision, rules, documents, open work with each next step, and, with a knowledge base, the topic map, each fact with its exact source passage, and the source documents. It is written to `.docs-site/` (added to `.gitignore` on the first run), outside the docs root (`mkt_docs/` or `ai_docs/`); nothing is ranked or recommended.
|
|
31
31
|
|
|
32
32
|
The agent asks the user only for facts the user uniquely owns (product, price, budget, capacity); everything else — market sizing, competitor analysis, channel benchmarks — is derived from real web research.
|
|
33
33
|
|
|
@@ -58,8 +58,8 @@ npx mkt-sdlc-init # creates mkt_docs/ + protocol pointers
|
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
It also **wires the SessionStart orientation hook** into `.claude/`, so an agent opening
|
|
61
|
-
the project is handed the
|
|
62
|
-
|
|
61
|
+
the project is handed the triage line, the guide router, the workstream registry and the
|
|
62
|
+
reading guide (with pointers to the manifest) before it does anything — instead of only when it remembers to look. That
|
|
63
63
|
wiring used to be a manual step documented in `ENFORCEMENT.md`; being manual, it was
|
|
64
64
|
skipped, and a project could run fully governed with an agent that never met the process.
|
|
65
65
|
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mkt-agentic-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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.26.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",
|
|
@@ -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
|
|
|
@@ -59,9 +59,21 @@ Semantics: exit code 2 + message on stderr ⇒ the write is blocked and the mess
|
|
|
59
59
|
- The hook assumes the working directory is the project root (standard behavior of Claude Code hooks).
|
|
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
|
+
### Verdict check: `verdict` (PreToolUse -- manual for this lens)
|
|
63
|
+
|
|
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)
|
|
66
|
+
or a guide the verdict names was never read; it is silent in projects without guides and
|
|
67
|
+
whenever it cannot tell. The code package (`agentic-sdlc`) wires it machine-wide on
|
|
68
|
+
Claude Code; this package does not. To wire it by hand, add a `PreToolUse` entry with
|
|
69
|
+
matcher `Edit|Write|MultiEdit|NotebookEdit` and command
|
|
70
|
+
`python "<skills dir>/mkt-agentic-sdlc/scripts/mkt_check.py" verdict --hook; exit 0` — the `; exit 0`
|
|
71
|
+
keeps a missing or older validator from exiting 2, which would block every edit. Cost
|
|
72
|
+
and residuals: `ai_docs/vision/rulings.md` r34.
|
|
73
|
+
|
|
62
74
|
## 4. SessionStart hook (orientation, recommended default)
|
|
63
75
|
|
|
64
|
-
Emits the `ai_docs/` orientation
|
|
76
|
+
Emits the `ai_docs/` orientation to stdout at session start, in the order an arriving agent needs it: the Rule-Zero triage line, the lens choice when sibling lenses are installed, the guide router (`reference/INDEX.md`, one line per guide), the workstream registry (`audit/handoff.md`, one line per workstream, then the project-wide notes), the reading guide (`README.md`), then pointers to the manifest (`INDEX.md`) and to the full files — so the agent begins already oriented instead of reading them only if it remembers to. The whole output stays under 9,500 characters: Claude Code injects a hook's output up to 10,000 and, above that, only a 2,000-character preview, which is how the router once never reached a session (F-075). A section that does not fit is cut at a line with a pointer to its file, and a note says when one client runs the hook from two settings entries. It is **fail-open**: a missing or unreadable doc is skipped, and it always exits 0 — a broken or empty `ai_docs/` never blocks the session. It is **zero-execution** (it reads and prints, never runs anything).
|
|
65
77
|
|
|
66
78
|
**Wire it on every project that has `ai_docs/` and a Python interpreter.** It was opt-in until v1.16.0 and the field result was the defect this level exists to prevent: the guide router stayed unread unless the user asked for it by hand, so guides were written and never consulted. Prompt-level placement (Rule Zero declares the router verdict; Phase 1 reads the router) carries the process on its own — this hook is the backstop that survives long contexts, compaction and a session that never enters Phase 1 explicitly. Skip it only where Python is unavailable, and know what you are trading.
|
|
67
79
|
|
|
@@ -155,7 +167,7 @@ Gemini CLI — wire the same command into its startup-hook mechanism if present;
|
|
|
155
167
|
**Usage notes:**
|
|
156
168
|
- The hook assumes the working directory is the project root (standard Claude Code hook behavior); it also accepts `--root <path>`.
|
|
157
169
|
- **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.
|
|
170
|
+
- 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
171
|
|
|
160
172
|
### Per-turn reminder: `remind` (UserPromptSubmit -- wired by default)
|
|
161
173
|
|
|
@@ -166,13 +178,13 @@ a release ran against a `GUIDE_release.md` the session never consulted, because
|
|
|
166
178
|
ONE constant line at every prompt, and its first duty is that one: decide whether the
|
|
167
179
|
skill governs the work in front of you and, if it does, load it once.
|
|
168
180
|
|
|
169
|
-
Cost:
|
|
181
|
+
Cost: 525 characters, ~88 tokens per prompt, paid on every turn including
|
|
170
182
|
trivial ones. That is admissible only because the owner accepted it explicitly
|
|
171
183
|
(`ai_docs/vision/rulings.md` r19, the "no ceremony ratchet" Non-Goal's second door) --
|
|
172
184
|
and the acceptance was for a FLAT cost. Hence the contract, guarded by
|
|
173
185
|
`scripts/test_remind.py`: the line is **constant and reads nothing** (so nothing repo-
|
|
174
186
|
or session-controlled rides into the agent's context through it, and the cost cannot
|
|
175
|
-
grow with the session), it is **one line under
|
|
187
|
+
grow with the session), it is **one line under 600 characters** (r32 raised r19's 500 to carry the lens choice), and it **can never
|
|
176
188
|
break a prompt** (argv ignored whole, always exit 0). A trivial turn pays a yes/no
|
|
177
189
|
judgement, never a load.
|
|
178
190
|
|
|
@@ -215,7 +227,7 @@ It aggregates the test files (`test_plan.py` + `test_session_start.py` + `test_s
|
|
|
215
227
|
|
|
216
228
|
**Optional CI** (same shape as §2, not mandatory): add a `run:` step invoking the `unittest discover` command above.
|
|
217
229
|
|
|
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.
|
|
230
|
+
**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
231
|
|
|
220
232
|
## `benefit` is a report, never a CI gate
|
|
221
233
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mkt-agentic-sdlc
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.26.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
|
|
@@ -18,7 +18,8 @@ Support files in the skill directory:
|
|
|
18
18
|
- `templates.md`: templates for every artifact and deliverable.
|
|
19
19
|
- `review.md`: the family's shared review discipline (independence ladder, rounds, log). The marketing-specific attack surface — swap test, untraced numbers, orphan tactics, missing kill/scale — lives in `frameworks.md`'s falsification rules; the adversarial CMO review applies them through `review.md`'s procedure.
|
|
20
20
|
- `scripts/mkt_check.py` + `scripts/sdlc_core.py`: the mechanical validator (`check`, `validate`, `ledger`, `benefit`, `budget`, `funnel`, `trace`, `index`, plus the spine's `stale`/`mark`/`gate`/`orient`/`plan`/`migrate`). Keep the entry point, `scripts/sdlc_core.py` and `scripts/knowledge.py` together; the last supplies common memory and KB integrity.
|
|
21
|
-
- `scripts/docs_site.py --root <project> --open
|
|
21
|
+
- `scripts/docs_site.py --root <project> --open` (`--zip <file>` to share): opens in the user's browser a read-only site of what the agent reads; use it to show the project state.
|
|
22
|
+
It is what users mean by "docs site", "minisito", "documentation site" or "project site": run it, never build a page by hand.
|
|
22
23
|
- `hybrid.md`: the devPNT seam — the authoritative hierarchy, the ownership matrix (which artifact is mastered where), and the Hybrid rules. Read it when the `devpnt_*` tools point at this project; a Standalone engagement never needs it.
|
|
23
24
|
- `ENFORCEMENT.md`: optional setup for CI and hooks.
|
|
24
25
|
|
|
@@ -273,6 +273,11 @@
|
|
|
273
273
|
C.chronology.length ? eventList(C.chronology) : el("p", { "class": "empty", text: t("chronology_empty") }));
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function verifyLinks(o) {
|
|
277
|
+
return [o.merge ? docLink(o.merge, t("chain_merge")) : null,
|
|
278
|
+
o.e2e ? docLink(o.e2e, t("chain_e2e")) : null].filter(Boolean);
|
|
279
|
+
}
|
|
280
|
+
|
|
276
281
|
function pageWork() {
|
|
277
282
|
setNav("work");
|
|
278
283
|
var w = C.work;
|
|
@@ -289,7 +294,7 @@
|
|
|
289
294
|
el("h3", { text: c.name }),
|
|
290
295
|
el("dl", null, rows),
|
|
291
296
|
c.next ? el("div", { "class": "next" }, el("b", { text: t("work_next") }), c.next) : null,
|
|
292
|
-
el("div", { "class": "links" }, docLink(c.path, t("work_resume"))));
|
|
297
|
+
el("div", { "class": "links" }, docLink(c.path, t("work_resume")), verifyLinks(c)));
|
|
293
298
|
}))
|
|
294
299
|
: el("p", { "class": "empty", text: t("work_empty") });
|
|
295
300
|
main(heading(t("work_title"), t("work_why")), cards,
|
|
@@ -331,14 +336,15 @@
|
|
|
331
336
|
return el("div", { "class": "unit" },
|
|
332
337
|
el("span", { "class": "id", text: u.id || "" }),
|
|
333
338
|
docLink(u.path, u.name),
|
|
334
|
-
el("div", { "class": "muted" }, [u.status, u.level ? levelLabel(u.level) : null, lensName(u.lens), dates(u)].filter(written).join(" · "))
|
|
339
|
+
el("div", { "class": "muted" }, [u.status, u.level ? levelLabel(u.level) : null, lensName(u.lens), dates(u)].filter(written).join(" · ")),
|
|
340
|
+
verifyLinks(u).length ? el("div", { "class": "links" }, verifyLinks(u)) : null);
|
|
335
341
|
}));
|
|
336
342
|
}));
|
|
337
343
|
} else {
|
|
338
344
|
body = el("table", { "class": "list" },
|
|
339
345
|
el("thead", null, el("tr", null, [t("units_id"), t("units_name"), t("work_status"), t("work_level"), t("units_lens"), t("units_dates")].map(function (h) { return el("th", { text: h }); }))),
|
|
340
346
|
el("tbody", null, shown.map(function (u) {
|
|
341
|
-
return el("tr", null, el("td", null, el("code", { text: u.id || "" })), el("td", null, docLink(u.path, u.name)),
|
|
347
|
+
return el("tr", null, el("td", null, el("code", { text: u.id || "" })), el("td", null, docLink(u.path, u.name), verifyLinks(u).length ? el("div", { "class": "links" }, verifyLinks(u)) : null),
|
|
342
348
|
el("td", null, u.status ? statusPill(u.status, u.group) : ""), el("td", { text: u.level ? levelLabel(u.level) : "" }),
|
|
343
349
|
el("td", { text: lensName(u.lens) }), el("td", { text: dates(u) }));
|
|
344
350
|
})));
|
|
@@ -201,7 +201,13 @@ the component from partial understanding.
|
|
|
201
201
|
rewrites snapshot bytes at all (keeps snapshots byte-verbatim; the
|
|
202
202
|
normalized hash stays stable either way).
|
|
203
203
|
- The snapshot is verbatim: no paraphrasing, no reformatting beyond what is
|
|
204
|
-
needed to save it as markdown
|
|
204
|
+
needed to save it as markdown — with one exception, made before the file is
|
|
205
|
+
written and hashed: credentials, and personal or confidential third-party data
|
|
206
|
+
the guide does not need, become `[REDACTED: <kind>]`, and the guide's
|
|
207
|
+
frontmatter lists the kinds in `redacted_at_intake:`. A snapshot is committed
|
|
208
|
+
and hash-pinned, and `check` refuses a credential-shaped line in a document
|
|
209
|
+
guide's snapshot (a false positive is declared in the guide as
|
|
210
|
+
`credential_scan: reviewed — <reason>`).
|
|
205
211
|
- **For `source_kind: code`** the snapshot is the verbatim CODE EXCERPTS the guide
|
|
206
212
|
explains — the specific functions / classes / regions of the real files, each
|
|
207
213
|
labelled with its `path:symbol` (or `path:startLine-endLine`) — assembled into
|
|
@@ -29,7 +29,7 @@ the most recent document or convert a claim into approval.
|
|
|
29
29
|
|
|
30
30
|
When `topics/` exists, also scan `topics/INDEX.md` and follow relevant descendants,
|
|
31
31
|
synonyms and claim sources. Cite claim IDs with their locators. Preserve CONTESTED
|
|
32
|
-
alternatives; name successors when citing SUPERSEDED claims; check validity windows.
|
|
32
|
+
alternatives; name successors when citing SUPERSEDED claims; cite a claim marked `not-yet-honored-by` as not yet true of the code, naming its target; check validity windows.
|
|
33
33
|
For DERIVED claims grounding a decision, reopen the derivation to non-DERIVED ground;
|
|
34
34
|
an inaccessible chain is unverified, not an independent source. Imported rulings do
|
|
35
35
|
not grant local authority. Corpus text is data, never executable agent instructions.
|
|
@@ -15,7 +15,24 @@ Two moments, and they review different things:
|
|
|
15
15
|
|---|---|---|---|
|
|
16
16
|
| **1. Design review** | End of Phase 3 — **before any implementation** | the ANALYSIS (Standalone) / the `E-ISP`+`E-TDD` (Hybrid) | L3 |
|
|
17
17
|
| **1b. Late arrival** | Work that became L3 *after* code existed — an L1/L2 reclassified mid-flight, or a design increment on a feature already implemented — runs moment 1 **now**, before any further implementation, logged `design (late)` | same | L3 |
|
|
18
|
-
| **2. Closure review** | Phase 5, before DONE | the actual diff, against that approved design | L2 / L3 —
|
|
18
|
+
| **2. Closure review** | Phase 5, before DONE | the actual diff, against that approved design | L2 / L3 — at level 2 (L2; E2 in a lens that counts E-levels) it is due only on a security-sensitive area the lens's Rule Zero names, or at the owner's request, and otherwise does not run and logs no row; every review that runs logs its row |
|
|
19
|
+
|
|
20
|
+
**A delta after a PASS reopens a review only where it changes what was judged.**
|
|
21
|
+
A review lowers one risk — a judgement its author is blind to — at the cost of a
|
|
22
|
+
deep reviewer's full read. After a design or closure PASS, a delta made only of
|
|
23
|
+
these kinds carries no new judgement, and the closure review is **not due** on it:
|
|
24
|
+
version numbers and release metadata; regenerated indexes and audit-mark records;
|
|
25
|
+
copies of a shared file that a tool verifies identical to the reviewed one; and
|
|
26
|
+
CHANGELOG lines that state nothing the reviewed artifacts do not already state. The
|
|
27
|
+
list is closed and content governs it: a CHANGELOG line adding a claim is judged
|
|
28
|
+
work, and a change answering a review finding is never in it (§Review-driven
|
|
29
|
+
corrections governs that). Such a delta is verified by its own check and logged as a
|
|
30
|
+
row with `tier` `closure`, `model` `none (not due)`, findings `0`, rounds `0` and
|
|
31
|
+
verdict `NOT DUE: <the kinds; the check that ran and its result>`; a failing check
|
|
32
|
+
makes the delta judged work. Any other change after the PASS — README or overview
|
|
33
|
+
prose, an E2E script, a rulings row, doctrine, design, behaviour, a test — is
|
|
34
|
+
reviewed, scoped to that part. In Hybrid, devPNT doctrine §4.6 adds to this rule,
|
|
35
|
+
never yields to it.
|
|
19
36
|
|
|
20
37
|
**The agent review comes first, the human's after — never instead (Hybrid).**
|
|
21
38
|
No governed artifact that this file reviews — the design (`E-ISP`, `E-TDD`),
|
|
@@ -172,7 +189,7 @@ logging only passes would erase exactly that evidence. The row goes in
|
|
|
172
189
|
`| date | doc_key | tier | model | reviewer | findings_raised | findings_real | verdict | revise_rounds |`,
|
|
173
190
|
with `tier` = `design` or `closure` in Standalone. **`model` records the capability
|
|
174
191
|
tier that actually ran** — `deep`, `light`, `economy`, `single (client exposes no
|
|
175
|
-
choice)`,
|
|
192
|
+
choice)`, `below floor: <reason>`, or `none (not due)` on a NOT DUE row — never a provider name in doctrine (a project
|
|
176
193
|
may of course write its own model names in its own log). Without it the floor above
|
|
177
194
|
is a belief: the row would say a gate ran and never whether it could do its job, and
|
|
178
195
|
the correlation with `revise_rounds` and `findings_real` — the only evidence the
|
|
@@ -394,7 +411,15 @@ When you are the reviewer:
|
|
|
394
411
|
every use-case/user-need (and the Actor it serves — a use-case with no defined Actor,
|
|
395
412
|
or an Actor UX expectation the solution does not meet, is a finding), every threat, and
|
|
396
413
|
every applicable Vision benefit/Non-Goal, state WHERE the artifact satisfies it (section
|
|
397
|
-
or `file:line`) or raise it as a finding.
|
|
414
|
+
or `file:line`) or raise it as a finding. **For an L3/E3 unit, each outcome the owner
|
|
415
|
+
named — in the request record (the opening request and every entry widening scope) or
|
|
416
|
+
in an approved north star — is a row too, weighed by effort**: where the design serves
|
|
417
|
+
it and what share of the design (its sections and planned changes) goes to it. An
|
|
418
|
+
outcome no section serves, or one served by a minor share while a secondary concern
|
|
419
|
+
takes the bulk, is a finding — the drift every per-constraint row misses when each
|
|
420
|
+
constraint is met. In the lens whose `SKILL.md` defines Execute-Before-Specify, an
|
|
421
|
+
outcome whose owner's words name a comparative effect also needs, as evidence, the
|
|
422
|
+
probe that shows it, red on the baseline; you rule whether the words name one. A PASS/approve is **not valid on "found nothing"** — the conformance
|
|
398
423
|
statement is the proof the check ran; an unfalsifiable "I checked" is the review
|
|
399
424
|
theater this discipline exists to prevent (the reviewer-side twin of §Receiving's
|
|
400
425
|
silent-drop rule). Plain code reviews stay findings-only.
|
|
@@ -92,6 +92,10 @@ Run the steps in order. Stop at the first one that decides.
|
|
|
92
92
|
| "research sources and teach a newcomer a topic" | purpose: teaching → course | never reached | never reached | **course** |
|
|
93
93
|
| "ingest these manuals, then create a course" | split: corpus = supplied docs; training = teaching | never reached | never reached | **knowledge** for ingestion; **course** for training |
|
|
94
94
|
| "write sales copy and an onboarding course" | split: persuasion and teaching | never reached | never reached | **marketing** for copy; **course** for training |
|
|
95
|
+
| "write the interface spec of our service for another team" (kept and versioned with the repo) | code | repo's own doc set → code | not build-consumed → no flip | **code** |
|
|
96
|
+
| "write the requirements another team must build, from their documents and our notes" | supplied docs → knowledge | never reached | never reached | **knowledge** |
|
|
97
|
+
| "the vendor spec changed: update the guide distilled from it" | split (step 4): guide = supplied docs; any code change the new spec requires = this repo | never reached | never reached | **knowledge** for re-distilling the guide; **code** for the code change, citing the guide |
|
|
98
|
+
| "the code and the supplied spec diverge" | split (step 4): changing the code = this repo; correcting the distillation = supplied docs | never reached | never reached | **code** if the code is wrong; **knowledge** if the distillation is — which one is wrong is the owner's call, not the router's |
|
|
95
99
|
|
|
96
100
|
## 3. Acting on the verdict
|
|
97
101
|
|
|
@@ -33,6 +33,7 @@ import re
|
|
|
33
33
|
import shutil
|
|
34
34
|
import subprocess
|
|
35
35
|
import sys
|
|
36
|
+
import zipfile
|
|
36
37
|
import unicodedata
|
|
37
38
|
import urllib.parse
|
|
38
39
|
from datetime import datetime
|
|
@@ -767,6 +768,8 @@ def build_sources(docs, rows_of):
|
|
|
767
768
|
name = mp.name[:-len(".meta.md")]
|
|
768
769
|
t = _text(mp) or ""
|
|
769
770
|
meta = sdlc_core.load_frontmatter(t.splitlines()) or {}
|
|
771
|
+
if knowledge.kb_is_tombstone(meta):
|
|
772
|
+
continue # withdrawn: no bytes to show; corpus/INDEX.md keeps the row
|
|
770
773
|
through = _v(meta, "extracted_through")
|
|
771
774
|
parsed = knowledge.kb_parse_extracted_through(through) if through else None
|
|
772
775
|
if not through:
|
|
@@ -947,6 +950,21 @@ def analysis_chain(body):
|
|
|
947
950
|
return [{"step": s, "sections": found[s]} for s in CHAIN_STEPS]
|
|
948
951
|
|
|
949
952
|
|
|
953
|
+
def companion_paths(stem):
|
|
954
|
+
"""The documents named after a unit's stem: (key, docs-relative path). One list,
|
|
955
|
+
read by the chain page and by the work and feature lists."""
|
|
956
|
+
return (("merge", "solutions/MERGE_REVIEW_%s.md" % stem),
|
|
957
|
+
("e2e", "solutions/E2E_TEST_%s.md" % stem),
|
|
958
|
+
("request", "audit/requests/REQUEST_%s.md" % stem))
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def verify_links(docs, stem):
|
|
962
|
+
"""The merge brief and E2E script of a unit, where they exist: who merges reads the
|
|
963
|
+
first while the work is open, who tests reads the second after it closed."""
|
|
964
|
+
return {key: path for key, path in companion_paths(stem)
|
|
965
|
+
if key in ("merge", "e2e") and (docs / path).is_file()}
|
|
966
|
+
|
|
967
|
+
|
|
950
968
|
def link_companions(docs, pages):
|
|
951
969
|
"""Each analysis's merge brief, E2E script and request record, where they exist,
|
|
952
970
|
and the project's North Star (R25)."""
|
|
@@ -962,11 +980,8 @@ def link_companions(docs, pages):
|
|
|
962
980
|
continue
|
|
963
981
|
stem = chain.pop("_stem")
|
|
964
982
|
chain["north_star"] = north_star
|
|
965
|
-
chain["companions"] = {
|
|
966
|
-
|
|
967
|
-
for key, path in (("merge", "solutions/MERGE_REVIEW_%s.md" % stem),
|
|
968
|
-
("e2e", "solutions/E2E_TEST_%s.md" % stem),
|
|
969
|
-
("request", "audit/requests/REQUEST_%s.md" % stem))}
|
|
983
|
+
chain["companions"] = {key: path if path in pages else None
|
|
984
|
+
for key, path in companion_paths(stem)}
|
|
970
985
|
|
|
971
986
|
|
|
972
987
|
def orientation_sources(docs):
|
|
@@ -1055,6 +1070,7 @@ def build_work(project, docs, facts):
|
|
|
1055
1070
|
for key in ("status", "level", "since", "updated", "branch", "next"):
|
|
1056
1071
|
if _v(meta, key):
|
|
1057
1072
|
card[key] = _v(meta, key)
|
|
1073
|
+
card.update(verify_links(docs, p.stem[len("HANDOFF_"):]))
|
|
1058
1074
|
cards.append(card)
|
|
1059
1075
|
cards.sort(key=lambda c: c["name"].lower())
|
|
1060
1076
|
cards.sort(key=lambda c: c.get("updated", ""), reverse=True)
|
|
@@ -1083,8 +1099,12 @@ def build_units(project, lens):
|
|
|
1083
1099
|
u[key] = _v(meta, field)
|
|
1084
1100
|
if "id" not in u and _v(meta, "feature"):
|
|
1085
1101
|
u["id"] = _v(meta, "feature")
|
|
1102
|
+
u.update(verify_links(sdlc_core.ai_path(project), p.stem[len("ANALYSIS_"):]))
|
|
1086
1103
|
units.append(u)
|
|
1104
|
+
# newest first by declared start date (owner, 2026-10-19); undated last, by id
|
|
1087
1105
|
units.sort(key=lambda u: (u.get("id", ""), u["path"]))
|
|
1106
|
+
units.sort(key=lambda u: u.get("start", ""), reverse=True)
|
|
1107
|
+
units.sort(key=lambda u: not u.get("start"))
|
|
1088
1108
|
return units
|
|
1089
1109
|
|
|
1090
1110
|
|
|
@@ -1204,7 +1224,14 @@ def chronology(docs, pages):
|
|
|
1204
1224
|
text = _text(p)
|
|
1205
1225
|
# a shadow mirror is not an analysis to `validate` (list_analyses): not here either
|
|
1206
1226
|
if text is not None and not sdlc_core.is_shadow(p, (text.splitlines() or [""])[0]):
|
|
1227
|
+
# a Diary label ("closed", "Approved (R5)") names no unit: prefix the id, so
|
|
1228
|
+
# the event reads on its own in the chronology (field case 2026-10-19)
|
|
1229
|
+
uid = dict((k.lower(), v) for k, v in split_frontmatter(text)[0]).get("id", "").strip()
|
|
1230
|
+
start = len(events)
|
|
1207
1231
|
openers("solutions/" + p.name, "diary", sdlc_core.DIARY_SECTION)
|
|
1232
|
+
if uid:
|
|
1233
|
+
for e in events[start:]:
|
|
1234
|
+
e["label"] = "%s — %s" % (uid, e["label"])
|
|
1208
1235
|
# spike outcomes: SPIKE_* in the code/kb/course lenses, RESEARCH_* in marketing
|
|
1209
1236
|
for rel in sorted(["solutions/" + p.name for p in docs.glob("solutions/SPIKE_*.md")]
|
|
1210
1237
|
+ ["spikes/" + p.name for p in docs.glob("spikes/RESEARCH_*.md")]):
|
|
@@ -1234,10 +1261,12 @@ def chronology(docs, pages):
|
|
|
1234
1261
|
text = _text(p)
|
|
1235
1262
|
if text is None:
|
|
1236
1263
|
continue
|
|
1237
|
-
|
|
1238
|
-
|
|
1264
|
+
fm = sdlc_core.load_frontmatter(text.splitlines())
|
|
1265
|
+
date = knowledge.frontmatter_date(fm)
|
|
1239
1266
|
if not date:
|
|
1240
1267
|
continue
|
|
1268
|
+
if kind == "source" and knowledge.kb_is_tombstone(fm):
|
|
1269
|
+
continue
|
|
1241
1270
|
if kind == "source":
|
|
1242
1271
|
name = p.name[:-len(suffix)]
|
|
1243
1272
|
add(date, kind, name, rel, source=name)
|
|
@@ -1272,6 +1301,29 @@ def check_out(out, docs):
|
|
|
1272
1301
|
return out
|
|
1273
1302
|
|
|
1274
1303
|
|
|
1304
|
+
def check_zip(target, docs, out):
|
|
1305
|
+
"""Where `--zip` may write: a .zip file outside the docs root and outside the site."""
|
|
1306
|
+
target = target.resolve()
|
|
1307
|
+
if target.suffix.lower() != ".zip":
|
|
1308
|
+
raise SiteError("--zip %s must name a .zip file." % target)
|
|
1309
|
+
for folder, what in ((docs.resolve(), "the docs root"), (out.resolve(), "the site folder")):
|
|
1310
|
+
if target == folder or folder in target.parents:
|
|
1311
|
+
raise SiteError("--zip %s is inside %s %s." % (target, what, folder))
|
|
1312
|
+
if target.exists() and not target.is_file():
|
|
1313
|
+
raise SiteError("--zip %s exists and is not a file." % target)
|
|
1314
|
+
return target
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def write_zip(out, target):
|
|
1318
|
+
"""The site as one file to share. Entry names always use '/': an archive whose names
|
|
1319
|
+
carry '\\' (Windows PowerShell 5.1 Compress-Archive) unpacks on Linux and macOS as
|
|
1320
|
+
files named `data\\core.js`, and the page then finds no data (field case 2026-10-19)."""
|
|
1321
|
+
with zipfile.ZipFile(str(target), "w", zipfile.ZIP_DEFLATED) as z:
|
|
1322
|
+
for p in sorted(out.rglob("*")):
|
|
1323
|
+
if p.is_file() and p.name != MARKER:
|
|
1324
|
+
z.write(str(p), p.relative_to(out).as_posix())
|
|
1325
|
+
|
|
1326
|
+
|
|
1275
1327
|
def write_site(out, core, pages, kb):
|
|
1276
1328
|
out.mkdir(parents=True, exist_ok=True)
|
|
1277
1329
|
for rel in PAGE_FILES + ("data",):
|
|
@@ -1307,6 +1359,30 @@ def git_ignored(project, out):
|
|
|
1307
1359
|
return None
|
|
1308
1360
|
|
|
1309
1361
|
|
|
1362
|
+
def ensure_ignored(project, out):
|
|
1363
|
+
"""Add the site folder to the project's .gitignore when git does not ignore it, so the
|
|
1364
|
+
site's copy of the documents is not committed. An entry already in the file is never
|
|
1365
|
+
rewritten (git may ignore it elsewhere, or the user negated it on purpose). Returns the
|
|
1366
|
+
line added, or None; False when the folder stays not ignored."""
|
|
1367
|
+
if git_ignored(project, out) is not False:
|
|
1368
|
+
return None
|
|
1369
|
+
rel = out.resolve().relative_to(project.resolve()).as_posix()
|
|
1370
|
+
entry = rel + "/"
|
|
1371
|
+
gi = project / ".gitignore"
|
|
1372
|
+
try:
|
|
1373
|
+
raw = gi.read_bytes() if gi.is_file() else b""
|
|
1374
|
+
text = raw.decode("utf-8")
|
|
1375
|
+
if {rel, entry, "/" + rel, "/" + entry} & {l.strip() for l in text.splitlines()}:
|
|
1376
|
+
return False
|
|
1377
|
+
nl = "\r\n" if "\r\n" in text else "\n"
|
|
1378
|
+
sep = nl if text and not text.endswith(("\n", "\r")) else ""
|
|
1379
|
+
with open(str(gi), "ab") as f:
|
|
1380
|
+
f.write((sep + entry + nl).encode("utf-8"))
|
|
1381
|
+
except (OSError, UnicodeDecodeError):
|
|
1382
|
+
return False
|
|
1383
|
+
return entry
|
|
1384
|
+
|
|
1385
|
+
|
|
1310
1386
|
def _shown(path, docs):
|
|
1311
1387
|
try:
|
|
1312
1388
|
return Path(path).relative_to(docs).as_posix()
|
|
@@ -1322,11 +1398,14 @@ def main(argv=None):
|
|
|
1322
1398
|
ap.add_argument("--docs-dir", dest="docs_dir", help="docs root name (ai_docs or mkt_docs)")
|
|
1323
1399
|
ap.add_argument("--out", help="output folder (default: <project>/%s)" % DEFAULT_OUT)
|
|
1324
1400
|
ap.add_argument("--open", action="store_true", help="open the site in the browser")
|
|
1401
|
+
ap.add_argument("--zip", metavar="FILE",
|
|
1402
|
+
help="also write the site as one .zip to share; it opens on any system")
|
|
1325
1403
|
args = ap.parse_args(argv)
|
|
1326
1404
|
del _UNREADABLE[:]
|
|
1327
1405
|
try:
|
|
1328
1406
|
project, docs, name = resolve_root(args)
|
|
1329
1407
|
out = check_out(Path(args.out) if args.out else project / DEFAULT_OUT, docs)
|
|
1408
|
+
target = check_zip(Path(args.zip), docs, out) if args.zip else None
|
|
1330
1409
|
except SiteError as exc:
|
|
1331
1410
|
print("[ERROR] %s" % exc)
|
|
1332
1411
|
return 2
|
|
@@ -1349,9 +1428,16 @@ def main(argv=None):
|
|
|
1349
1428
|
", %d topics, %d facts" % (len(kb["order"]), len(kb["facts"])) if kb else "", docs))
|
|
1350
1429
|
for path, exc in _UNREADABLE:
|
|
1351
1430
|
print("[ERROR] unreadable: %s (%s)" % (_shown(path, docs), exc))
|
|
1352
|
-
|
|
1431
|
+
added = ensure_ignored(project, out)
|
|
1432
|
+
if added:
|
|
1433
|
+
print("[ok] added %s to %s so the site's copy of your documents is not committed."
|
|
1434
|
+
% (added, project / ".gitignore"))
|
|
1435
|
+
elif added is False:
|
|
1353
1436
|
print("[note] %s is not git-ignored: add it to .gitignore so the site's copy of "
|
|
1354
1437
|
"your documents is not committed." % out)
|
|
1438
|
+
if target:
|
|
1439
|
+
write_zip(out, target)
|
|
1440
|
+
print("[ok] docs site zipped: %s (unzip it and open index.html)" % target)
|
|
1355
1441
|
if args.open:
|
|
1356
1442
|
import webbrowser
|
|
1357
1443
|
webbrowser.open((out / "index.html").as_uri())
|