maestro-case-kit 0.1.0__tar.gz

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.
@@ -0,0 +1,111 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ *.egg
9
+
10
+ # Virtual environments
11
+ .venv/
12
+ venv/
13
+ ENV/
14
+
15
+ # Environment variables
16
+ .env
17
+ .env.*
18
+ !.env.example
19
+
20
+ # IDE
21
+ .idea/
22
+ *.swp
23
+ *.swo
24
+ *~
25
+
26
+ # OS
27
+ .DS_Store
28
+ Thumbs.db
29
+
30
+ # Testing
31
+ .pytest_cache/
32
+ .coverage
33
+ htmlcov/
34
+ .mypy_cache/
35
+
36
+ # Databases (local dev)
37
+ *.db-shm
38
+ *.db-wal
39
+ ruvector.db
40
+
41
+ # Swarm state (ephemeral)
42
+ .swarm/
43
+
44
+ # Claude memory (session-specific)
45
+ .claude/memory.db
46
+
47
+ # Agent-OS session-persistence DB (churns per checkpoint; dir tracked via .keep)
48
+ .agent-os/memory/project_memory.db
49
+ __pycache__/
50
+
51
+ # UiPath solution per-user debug-overwrite state (generated by `solution project add`)
52
+ maestro_case/clearflow-solution/userProfile/
53
+
54
+ # Node
55
+ node_modules/
56
+ ui/node_modules/
57
+ ui/.next/
58
+ ui/out/
59
+
60
+ # Build artifacts
61
+ *.nupkg
62
+ .uipath/
63
+
64
+ # Coded-agent `uip codedagent init` scaffolding (regenerated per dir; not source)
65
+ agents/*/.agent/
66
+ agents/*/.claude/
67
+ agents/*/AGENTS.md
68
+ agents/*/CLAUDE.md
69
+ agents/*/main.mermaid
70
+
71
+ # UiPath skills repo (cloned locally, symlinked into .claude/skills etc.)
72
+ .uipath-skills/
73
+
74
+ # Installed UiPath skills (upstream content, sync via scripts/sync-uipath-skills.sh)
75
+ .claude/skills/uipath-*
76
+ .agents/skills/uipath-*
77
+ .github/skills/uipath-*
78
+
79
+ # Claude-flow runtime state
80
+ .claude-flow/
81
+
82
+ # Caches
83
+ .cache/
84
+
85
+ # Logs
86
+ *.log
87
+ scripts/logs/
88
+ .claude-flow/logs/
89
+
90
+ # Reference-only assets — never committed (images, PDFs, captured docs, archives).
91
+ # These live under knowledge/ for local reference only.
92
+ *.pdf
93
+ *.png
94
+ *.jpg
95
+ *.jpeg
96
+ *.gif
97
+ *.webp
98
+ # Exception: the architecture diagram is a deliberate committed submission asset
99
+ # (rendered from docs/images/architecture.svg), embedded in README + Devpost.
100
+ !docs/images/architecture.png
101
+ !docs/images/architecture.gif
102
+ *.zip
103
+ knowledge/new_knowledge/
104
+ knowledge/screenshots/studio_web_errors/
105
+ knowledge/agent-harness.zip
106
+
107
+ # Secret-bearing config — NEVER committed (MCP server API keys, test configs).
108
+ # Tracked previously and purged from history 2026-05-31; keys rotated.
109
+ .mcp.json
110
+ **/test_config.py
111
+ tests/unit/test_config.py
@@ -0,0 +1,44 @@
1
+ # Contributing to Maestro Case Kit
2
+
3
+ The knowledge layer is the moat — it stays valuable only if entries are accurate,
4
+ version-stamped, and free of any real-world identifiers. Every contribution runs
5
+ through an automated **schema + IP-safety gate** before it can land.
6
+
7
+ ## Add or update a knowledge entry
8
+
9
+ Entries live in [`src/maestro_case_kit/data/knowledge.json`](src/maestro_case_kit/data/knowledge.json).
10
+ Each entry has this shape:
11
+
12
+ | Field | Required | Notes |
13
+ |---|---|---|
14
+ | `id` | yes | Stable, unique, UPPER-KEBAB (e.g. `MC-SPAWN-QEM-400300`). |
15
+ | `kind` | yes | `runtime` / `data-fabric` / `hitl` / `cli` / `packaging` / `context-grounding` / ... |
16
+ | `title` | yes | One line. |
17
+ | `surface` | yes | Where it bites (e.g. `Maestro Case spawn JobArguments`). |
18
+ | `symptom` | yes | What the author observes. |
19
+ | `error_signatures` | list | Raw signatures `explain` matches on (`400300`, `still being indexed`); `[]` if silent. |
20
+ | `cause` | yes | Why it happens. |
21
+ | `fix` | yes | The proven workaround. |
22
+ | `proven_on` | yes | Platform/CLI version the behavior was confirmed on (e.g. `1.0.21`). |
23
+ | `resolved_in` | optional | Set when UiPath ships a fix — the entry then drops from active guidance. |
24
+ | `severity` | yes | `high` / `medium` / `low`. |
25
+ | `references` | list | Repo-relative paths or doc URLs. |
26
+
27
+ **Freshness is a feature.** Do not delete a fixed entry — set `resolved_in` so the
28
+ history survives and `explain --include-resolved` can still surface it.
29
+
30
+ **IP-safety is zero-tolerance.** No real company, product, patient, or claim names in
31
+ any field. Use generic or fictional placeholders.
32
+
33
+ ## Run the gate locally
34
+
35
+ ```bash
36
+ # Validate the bundled knowledge layer (schema + duplicate ids):
37
+ maestro-case validate-knowledge
38
+
39
+ # Validate a file against your own denylist (newline-delimited tokens, # comments ok):
40
+ maestro-case validate-knowledge --file path/to/knowledge.json --denylist-file denylist.txt
41
+ ```
42
+
43
+ A non-zero exit means the gate found problems — fix them before opening a PR. Add a
44
+ test for any new validator rule or behavior; `pytest` must be green.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: maestro-case-kit
3
+ Version: 0.1.0
4
+ Summary: Agent-native knowledge layer + offline, credential-free static validators for UiPath Maestro Case footguns.
5
+ License: MIT
6
+ Keywords: agent-skills,case-management,linter,maestro,mcp,uipath
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Maestro Case Kit
11
+
12
+ Offline, credential-free **knowledge + static validators** for UiPath **Maestro Case /
13
+ Data Fabric / Action Center** footguns. One define-once source, four artifacts: a Go-free
14
+ Python **CLI**, an **MCP server**, a Claude Code **skill**, and an OpenClaw **skill**.
15
+
16
+ > Built from behaviors discovered while running a multi-stakeholder crisis case end-to-end
17
+ > on UiPath Automation Cloud. The orchestration tier *above* the canvas is unserved by
18
+ > official tooling; this kit makes the hard-won knowledge installable and agent-native.
19
+
20
+ ## Why
21
+
22
+ - UiPath's coding-agent MCP is a single catch-all `run_command` shell — not typed tools.
23
+ - Maestro Case error codes (`400300`, `160009`, `170015`, ...) return zero search results.
24
+ - Caseplan edits can be silently inert; Data Fabric fields can silently vanish on insert.
25
+
26
+ This kit encodes those footguns as a **version-stamped knowledge layer** + **CI linters**
27
+ that run with **no UiPath login**.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pipx install maestro-case-kit # CLI: maestro-case ; MCP: maestro-case-mcp
33
+ ```
34
+
35
+ ## Use
36
+
37
+ ```bash
38
+ maestro-case explain 400300 # error code -> proven cause + fix (offline)
39
+ maestro-case lint path/to/caseplan-dir # static V20 lint (stale .bpmn, no start event, ...)
40
+ maestro-case check-spawn path/to/caseplan-dir # =datafabric.qem in spawn inputs (400300)
41
+ maestro-case check-df entity-spec.json # Data Fabric underscore-drop / reserved id
42
+ ```
43
+
44
+ Every command takes `--json` and exits non-zero when it has a finding, so it drops
45
+ straight into CI. See [`SKILL.md`](SKILL.md) for agent-host usage and recipes.
46
+
47
+ ## MCP server
48
+
49
+ `maestro-case-mcp` speaks newline-delimited JSON-RPC over stdio (no third-party MCP SDK
50
+ dependency) and exposes typed tools: `maestro_case_explain`, `maestro_case_lint`,
51
+ `maestro_case_check_spawn`, `maestro_case_check_df`. Register it with any MCP host.
52
+
53
+ ## One source, many harnesses
54
+
55
+ `SKILL.md` is the define-once skill source. Fan it out to other coding-agent runtimes
56
+ (Cursor, Codex, Gemini, Copilot, OpenClaw/ClawHub) with a skills converter — e.g.
57
+ `/polyskill` or `npx skills add`. The CLI and MCP server are generated from the same
58
+ shared tool registry (`maestro_case_kit/tools.py`), so a fix lands once and every surface
59
+ inherits it.
60
+
61
+ ## Knowledge entries & contributions
62
+
63
+ Entries live in `maestro_case_kit/data/knowledge.json`, each stamped with the version it
64
+ was proven on and an optional `resolved_in`. Contributions run through an automated
65
+ IP-safety + schema gate (see `CONTRIBUTING.md`). License: MIT.
@@ -0,0 +1,56 @@
1
+ # Maestro Case Kit
2
+
3
+ Offline, credential-free **knowledge + static validators** for UiPath **Maestro Case /
4
+ Data Fabric / Action Center** footguns. One define-once source, four artifacts: a Go-free
5
+ Python **CLI**, an **MCP server**, a Claude Code **skill**, and an OpenClaw **skill**.
6
+
7
+ > Built from behaviors discovered while running a multi-stakeholder crisis case end-to-end
8
+ > on UiPath Automation Cloud. The orchestration tier *above* the canvas is unserved by
9
+ > official tooling; this kit makes the hard-won knowledge installable and agent-native.
10
+
11
+ ## Why
12
+
13
+ - UiPath's coding-agent MCP is a single catch-all `run_command` shell — not typed tools.
14
+ - Maestro Case error codes (`400300`, `160009`, `170015`, ...) return zero search results.
15
+ - Caseplan edits can be silently inert; Data Fabric fields can silently vanish on insert.
16
+
17
+ This kit encodes those footguns as a **version-stamped knowledge layer** + **CI linters**
18
+ that run with **no UiPath login**.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pipx install maestro-case-kit # CLI: maestro-case ; MCP: maestro-case-mcp
24
+ ```
25
+
26
+ ## Use
27
+
28
+ ```bash
29
+ maestro-case explain 400300 # error code -> proven cause + fix (offline)
30
+ maestro-case lint path/to/caseplan-dir # static V20 lint (stale .bpmn, no start event, ...)
31
+ maestro-case check-spawn path/to/caseplan-dir # =datafabric.qem in spawn inputs (400300)
32
+ maestro-case check-df entity-spec.json # Data Fabric underscore-drop / reserved id
33
+ ```
34
+
35
+ Every command takes `--json` and exits non-zero when it has a finding, so it drops
36
+ straight into CI. See [`SKILL.md`](SKILL.md) for agent-host usage and recipes.
37
+
38
+ ## MCP server
39
+
40
+ `maestro-case-mcp` speaks newline-delimited JSON-RPC over stdio (no third-party MCP SDK
41
+ dependency) and exposes typed tools: `maestro_case_explain`, `maestro_case_lint`,
42
+ `maestro_case_check_spawn`, `maestro_case_check_df`. Register it with any MCP host.
43
+
44
+ ## One source, many harnesses
45
+
46
+ `SKILL.md` is the define-once skill source. Fan it out to other coding-agent runtimes
47
+ (Cursor, Codex, Gemini, Copilot, OpenClaw/ClawHub) with a skills converter — e.g.
48
+ `/polyskill` or `npx skills add`. The CLI and MCP server are generated from the same
49
+ shared tool registry (`maestro_case_kit/tools.py`), so a fix lands once and every surface
50
+ inherits it.
51
+
52
+ ## Knowledge entries & contributions
53
+
54
+ Entries live in `maestro_case_kit/data/knowledge.json`, each stamped with the version it
55
+ was proven on and an optional `resolved_in`. Contributions run through an automated
56
+ IP-safety + schema gate (see `CONTRIBUTING.md`). License: MIT.
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: maestro-case-kit
3
+ description: >-
4
+ Offline, credential-free knowledge + static validators for UiPath Maestro Case
5
+ footguns. Explain cryptic error codes (400300, 160009, ...) and lint caseplans,
6
+ spawn inputs, and Data Fabric specs in CI — no UiPath login. Use when authoring,
7
+ debugging, or reviewing UiPath Maestro Case / Data Fabric / Action Center work.
8
+ license: MIT
9
+ metadata:
10
+ homepage: https://github.com/mlbrilliance/cascadecare-network-command
11
+ openclaw: true
12
+ allowed-tools:
13
+ - Bash
14
+ ---
15
+
16
+ # Maestro Case Kit
17
+
18
+ A living, version-stamped knowledge layer over undocumented UiPath **Maestro Case /
19
+ Data Fabric / Action Center** footguns, plus credential-free static validators. The
20
+ v1 surface needs **no UiPath login** — it runs offline and in CI. The same surface
21
+ is exposed three ways from one source: a `maestro-case` CLI, an MCP server
22
+ (`maestro-case-mcp`, stdio), and this skill.
23
+
24
+ ## When to use
25
+
26
+ - An agent or developer hits a cryptic UiPath error code and search returns nothing.
27
+ - Before packing/deploying a Maestro Case, to catch inert-edit footguns in CI.
28
+ - Authoring Data Fabric entities, to avoid silent field-drops.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pipx install maestro-case-kit # or: pip install maestro-case-kit
34
+ # MCP server (stdio) — register with your agent host:
35
+ # command: maestro-case-mcp
36
+ ```
37
+
38
+ ## Commands (agent-native — every command takes --json and exits non-zero on findings)
39
+
40
+ - **Explain an error or footgun** (offline knowledge oracle):
41
+ ```bash
42
+ maestro-case explain 400300 # error code
43
+ maestro-case explain underscore # keyword
44
+ maestro-case explain 160009 --json # structured, for agents/CI
45
+ ```
46
+ - **Lint a caseplan directory** (stale .bpmn, missing start event, dup output vars, bad V20 expressions):
47
+ ```bash
48
+ maestro-case lint path/to/caseplan-dir --json
49
+ ```
50
+ - **Check spawn fan-out** (=datafabric.qem in spawn inputs → runtime 400300):
51
+ ```bash
52
+ maestro-case check-spawn path/to/caseplan-dir
53
+ ```
54
+ - **Check a Data Fabric entity spec** (underscore silent-drop, reserved `id`):
55
+ ```bash
56
+ maestro-case check-df entity-spec.json
57
+ ```
58
+
59
+ ## Recipe — wire the validators into CI
60
+
61
+ Run the three validators in a pre-deploy job; non-zero exit fails the build before a
62
+ broken caseplan or a data-losing entity ships:
63
+
64
+ ```bash
65
+ maestro-case lint "$CASEPLAN_DIR" \
66
+ && maestro-case check-spawn "$CASEPLAN_DIR" \
67
+ && maestro-case check-df "$ENTITY_SPEC"
68
+ ```
69
+
70
+ ## Recipe — explain a lint finding
71
+
72
+ Lint findings carry a rule id that maps to a knowledge entry; pass it to `explain`
73
+ for the full cause + fix:
74
+
75
+ ```bash
76
+ maestro-case lint "$CASEPLAN_DIR" --json | jq -r '.[].entry_id' | sort -u \
77
+ | xargs -I{} maestro-case explain {}
78
+ ```
79
+
80
+ ## Freshness
81
+
82
+ Every knowledge entry is stamped with the platform/CLI version it was proven on. When
83
+ UiPath fixes a footgun, the entry is marked `resolved_in` and drops from active
84
+ guidance — pass `--include-resolved` to `explain` to see history.
85
+
86
+ ## MCP tools
87
+
88
+ The MCP server exposes the same surface as typed tools: `maestro_case_explain`,
89
+ `maestro_case_lint`, `maestro_case_check_spawn`, `maestro_case_check_df`.
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "maestro-case-kit"
3
+ version = "0.1.0"
4
+ description = "Agent-native knowledge layer + offline, credential-free static validators for UiPath Maestro Case footguns."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = { text = "MIT" }
8
+ keywords = ["uipath", "maestro", "case-management", "linter", "mcp", "agent-skills"]
9
+ dependencies = []
10
+
11
+ [project.scripts]
12
+ maestro-case = "maestro_case_kit.cli:main"
13
+ maestro-case-mcp = "maestro_case_kit.mcp_server:main"
14
+
15
+ [build-system]
16
+ requires = ["hatchling"]
17
+ build-backend = "hatchling.build"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/maestro_case_kit"]
21
+
22
+ # Standalone test config (when the package is run on its own, post-extraction).
23
+ # The repo-level gate wires this package in via the root pyproject's pythonpath.
24
+ [tool.pytest.ini_options]
25
+ pythonpath = ["src"]
26
+ testpaths = ["tests"]
@@ -0,0 +1,9 @@
1
+ """Maestro Case Kit — agent-native knowledge + offline validators for UiPath Maestro Case.
2
+
3
+ A living, version-stamped knowledge layer over the undocumented Maestro Case /
4
+ Data Fabric / Action Center footguns surfaced while building a multi-stakeholder
5
+ crisis case, plus credential-free static validators that run in CI. No UiPath
6
+ login required for the v1 surface.
7
+ """
8
+
9
+ __version__ = "0.1.0"
@@ -0,0 +1,178 @@
1
+ """`maestro-case` CLI — agent-native, offline access to the knowledge layer.
2
+
3
+ v1 surface: `explain`. Validators (`lint`, `check-spawn`, `check-df`) attach to the
4
+ same parser in later slices. Every subcommand supports ``--json`` for agent/CI use
5
+ and exits non-zero when it has a finding to report.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import IO
15
+
16
+ from . import __version__, contribution, knowledge, validators
17
+
18
+
19
+ def _print_human(entries: list[knowledge.KnowledgeEntry], stream: IO[str]) -> None:
20
+ for e in entries:
21
+ signals = ", ".join(e.error_signatures) or "(no error code — silent behavior)"
22
+ resolved = f" resolved_in: {e.resolved_in}" if e.resolved_in else ""
23
+ print(f"[{e.id}] {e.title}", file=stream)
24
+ print(f" surface: {e.surface}", file=stream)
25
+ print(f" signals: {signals}", file=stream)
26
+ print(f" cause: {e.cause}", file=stream)
27
+ print(f" fix: {e.fix}", file=stream)
28
+ print(f" proven_on: {e.proven_on}{resolved}", file=stream)
29
+ if e.references:
30
+ print(f" refs: {', '.join(e.references)}", file=stream)
31
+ print("", file=stream)
32
+
33
+
34
+ def _cmd_explain(args: argparse.Namespace) -> int:
35
+ hits = knowledge.find(args.query, include_resolved=args.include_resolved)
36
+ if args.json:
37
+ print(json.dumps([e.to_dict() for e in hits], indent=2))
38
+ return 0 if hits else 1
39
+ if not hits:
40
+ print(
41
+ f"No known Maestro Case footgun matches {args.query!r}. "
42
+ f"Try an error code (e.g. 400300) or a keyword (e.g. underscore, gate, deploy).",
43
+ file=sys.stderr,
44
+ )
45
+ return 1
46
+ _print_human(hits, sys.stdout)
47
+ return 0
48
+
49
+
50
+ def _emit_findings(findings: list[validators.Finding], as_json: bool, ok_label: str) -> int:
51
+ if as_json:
52
+ print(json.dumps([f.to_dict() for f in findings], indent=2))
53
+ return 1 if findings else 0
54
+ if not findings:
55
+ print(f"OK — {ok_label}")
56
+ return 0
57
+ for f in findings:
58
+ suffix = f" (explain: {f.entry_id})" if f.entry_id else ""
59
+ where = f" @ {f.location}" if f.location else ""
60
+ print(f"[{f.severity.upper()}] {f.rule_id}: {f.message}{where}{suffix}")
61
+ print(f"\n{len(findings)} finding(s).")
62
+ return 1
63
+
64
+
65
+ def _cmd_lint(args: argparse.Namespace) -> int:
66
+ findings = validators.lint_caseplan(args.caseplan_dir)
67
+ return _emit_findings(findings, args.json, f"no Maestro Case footguns in {args.caseplan_dir}")
68
+
69
+
70
+ def _cmd_check_spawn(args: argparse.Namespace) -> int:
71
+ findings = validators.check_spawn_fanout(args.caseplan_dir)
72
+ return _emit_findings(findings, args.json, f"no qem spawn-fanout issues in {args.caseplan_dir}")
73
+
74
+
75
+ def _cmd_check_df(args: argparse.Namespace) -> int:
76
+ findings = validators.validate_df_entity(args.spec)
77
+ return _emit_findings(findings, args.json, f"no Data Fabric field-name traps in {args.spec}")
78
+
79
+
80
+ def _cmd_validate_knowledge(args: argparse.Namespace) -> int:
81
+ denylist: list[str] = []
82
+ if args.denylist_file:
83
+ for line in Path(args.denylist_file).read_text(encoding="utf-8").splitlines():
84
+ token = line.strip()
85
+ if token and not token.startswith("#"):
86
+ denylist.append(token)
87
+ data: object = args.file or {"entries": [e.to_dict() for e in knowledge.load_entries()]}
88
+ problems = contribution.validate_knowledge(data, denylist)
89
+ if args.json:
90
+ print(json.dumps(problems, indent=2))
91
+ return 1 if problems else 0
92
+ if not problems:
93
+ print("OK — knowledge passes the contribution gate.")
94
+ return 0
95
+ for problem in problems:
96
+ print(f"[GATE] {problem}")
97
+ print(f"\n{len(problems)} problem(s).")
98
+ return 1
99
+
100
+
101
+ def build_parser() -> argparse.ArgumentParser:
102
+ parser = argparse.ArgumentParser(
103
+ prog="maestro-case",
104
+ description="Knowledge + offline validators for UiPath Maestro Case footguns.",
105
+ )
106
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
107
+ sub = parser.add_subparsers(dest="command")
108
+
109
+ explain = sub.add_parser(
110
+ "explain",
111
+ help="Explain a UiPath Maestro Case error code or footgun from the knowledge layer.",
112
+ )
113
+ explain.add_argument(
114
+ "query",
115
+ help="An error code/signature (e.g. 400300, 160009) or a keyword (e.g. underscore, deploy).",
116
+ )
117
+ explain.add_argument("--json", action="store_true", help="Emit structured JSON.")
118
+ explain.add_argument(
119
+ "--include-resolved",
120
+ action="store_true",
121
+ help="Include entries marked resolved in a later platform version.",
122
+ )
123
+ explain.set_defaults(func=_cmd_explain)
124
+
125
+ lint = sub.add_parser(
126
+ "lint",
127
+ help="Statically lint a caseplan directory for known footguns (offline, no login).",
128
+ )
129
+ lint.add_argument(
130
+ "caseplan_dir",
131
+ help="Path to a directory containing caseplan.json (and ideally caseplan.json.bpmn).",
132
+ )
133
+ lint.add_argument("--json", action="store_true", help="Emit structured JSON findings.")
134
+ lint.set_defaults(func=_cmd_lint)
135
+
136
+ check_spawn = sub.add_parser(
137
+ "check-spawn",
138
+ help="Flag =datafabric.qem expressions in spawn inputs (fail at runtime, 400300).",
139
+ )
140
+ check_spawn.add_argument("caseplan_dir", help="Path to a directory containing caseplan.json.")
141
+ check_spawn.add_argument("--json", action="store_true", help="Emit structured JSON findings.")
142
+ check_spawn.set_defaults(func=_cmd_check_spawn)
143
+
144
+ check_df = sub.add_parser(
145
+ "check-df",
146
+ help="Lint a Data Fabric entity/field spec for silent-drop and reserved-name traps.",
147
+ )
148
+ check_df.add_argument("spec", help="Path to a JSON entity spec ({\"fields\": [...]}).")
149
+ check_df.add_argument("--json", action="store_true", help="Emit structured JSON findings.")
150
+ check_df.set_defaults(func=_cmd_check_df)
151
+
152
+ vk = sub.add_parser(
153
+ "validate-knowledge",
154
+ help="Validate a knowledge file against the schema + an optional IP-safety denylist.",
155
+ )
156
+ vk.add_argument("--file", help="Path to a knowledge JSON file (default: the bundled layer).")
157
+ vk.add_argument(
158
+ "--denylist-file",
159
+ help="Optional newline-delimited file of forbidden tokens (# comments allowed).",
160
+ )
161
+ vk.add_argument("--json", action="store_true", help="Emit structured JSON problems.")
162
+ vk.set_defaults(func=_cmd_validate_knowledge)
163
+ return parser
164
+
165
+
166
+ def main(argv: list[str] | None = None) -> int:
167
+ parser = build_parser()
168
+ args = parser.parse_args(argv)
169
+ if not getattr(args, "command", None):
170
+ parser.print_help(sys.stderr)
171
+ return 2
172
+ func = args.func
173
+ assert callable(func)
174
+ return int(func(args))
175
+
176
+
177
+ if __name__ == "__main__": # pragma: no cover
178
+ raise SystemExit(main())
@@ -0,0 +1,83 @@
1
+ """Contribution gate — validates knowledge entries against the schema and an
2
+ optional IP-safety denylist, so curation (not the distribution channel) is the moat.
3
+
4
+ A contributed entry must be well-formed and free of any caller-supplied forbidden
5
+ tokens (e.g. real company names). The denylist is a parameter, not hardcoded, so the
6
+ toolkit stays a general public artifact; a host repo passes its own denylist in CI.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections.abc import Iterable
13
+ from pathlib import Path
14
+
15
+ REQUIRED_FIELDS: tuple[str, ...] = (
16
+ "id",
17
+ "kind",
18
+ "title",
19
+ "surface",
20
+ "symptom",
21
+ "cause",
22
+ "fix",
23
+ "proven_on",
24
+ "severity",
25
+ )
26
+ VALID_SEVERITY: frozenset[str] = frozenset({"high", "medium", "low"})
27
+ _LIST_FIELDS: tuple[str, ...] = ("error_signatures", "references")
28
+
29
+
30
+ def _entry_text(entry: dict) -> str:
31
+ parts: list[str] = []
32
+ for value in entry.values():
33
+ if isinstance(value, str):
34
+ parts.append(value)
35
+ elif isinstance(value, list):
36
+ parts.extend(str(item) for item in value)
37
+ return " ".join(parts).lower()
38
+
39
+
40
+ def validate_entry(entry: dict, denylist: Iterable[str] = ()) -> list[str]:
41
+ """Return a list of problems with one entry; empty means it passes the gate."""
42
+ problems: list[str] = []
43
+ for field in REQUIRED_FIELDS:
44
+ if not entry.get(field):
45
+ problems.append(f"missing or empty required field: {field}")
46
+ severity = entry.get("severity")
47
+ if severity is not None and severity not in VALID_SEVERITY:
48
+ problems.append(f"invalid severity {severity!r} (expected one of {sorted(VALID_SEVERITY)})")
49
+ for field in _LIST_FIELDS:
50
+ if field in entry and not isinstance(entry[field], list):
51
+ problems.append(f"{field} must be a list")
52
+ text = _entry_text(entry)
53
+ entry_id = entry.get("id", "<unknown>")
54
+ for token in denylist:
55
+ if token and token.lower() in text:
56
+ problems.append(f"forbidden token {token!r} in entry {entry_id}")
57
+ return problems
58
+
59
+
60
+ def _coerce_entries(data: object) -> list[dict]:
61
+ if isinstance(data, (str, Path)):
62
+ data = json.loads(Path(data).read_text(encoding="utf-8"))
63
+ if isinstance(data, dict):
64
+ entries = data.get("entries", [])
65
+ else:
66
+ entries = data
67
+ return [e for e in entries if isinstance(e, dict)] if isinstance(entries, list) else []
68
+
69
+
70
+ def validate_knowledge(data: object, denylist: Iterable[str] = ()) -> list[str]:
71
+ """Validate a knowledge collection (path, {"entries": [...]}, or a list)."""
72
+ entries = _coerce_entries(data)
73
+ denylist = tuple(denylist)
74
+ problems: list[str] = []
75
+ ids: list[str] = []
76
+ for entry in entries:
77
+ problems.extend(validate_entry(entry, denylist))
78
+ entry_id = entry.get("id")
79
+ if isinstance(entry_id, str):
80
+ ids.append(entry_id)
81
+ for dup in sorted({i for i in ids if ids.count(i) > 1}):
82
+ problems.append(f"duplicate id: {dup}")
83
+ return problems