org-knowledge-layer 0.1.0__py3-none-any.whl
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.
- okl/__init__.py +12 -0
- okl/__main__.py +8 -0
- okl/bootstrap.py +83 -0
- okl/cli.py +484 -0
- okl/client.py +160 -0
- okl/core.py +223 -0
- okl/drift.py +119 -0
- okl/mcp_server.py +75 -0
- okl/scaffold/MANIFEST.md +59 -0
- okl/scaffold/ci/method-gates.yml +32 -0
- okl/scaffold/ci/okl-verify.yml +59 -0
- okl/scaffold/claude/agents/architecture-reviewer.md +41 -0
- okl/scaffold/claude/commands/check-rules.md +24 -0
- okl/scaffold/claude/commands/feature-spec.md +37 -0
- okl/scaffold/claude/rules/example-area.md +22 -0
- okl/scaffold/claude/skills/RECOMMENDED-COMPANIONS.md +40 -0
- okl/scaffold/claude/skills/encoding-loop/SKILL.md +48 -0
- okl/scaffold/claude/skills/verify-before-claiming/SKILL.md +56 -0
- okl/scaffold/evals/README.md +32 -0
- okl/scaffold/evals/cases.jsonl +1 -0
- okl/scaffold/evals/run_evals.py +109 -0
- okl/scaffold/gates/check-canon-size.sh +11 -0
- okl/scaffold/gates/check-doc-orphans.sh +19 -0
- okl/scaffold/gates/check-retractions.sh +22 -0
- okl/scaffold/gates/check-tombstones.sh +22 -0
- okl/scaffold/gates/run-gates.sh +31 -0
- okl/scaffold/hooks/hooks.json +16 -0
- okl/scaffold/hooks/stop-okl-encode.sh +78 -0
- okl/scaffold/hooks/userpromptsubmit-okl-check.sh +68 -0
- okl/scaffold/plugin/plugin.json +10 -0
- okl/scaffold/profiles/dotnet/README.md +12 -0
- okl/scaffold/profiles/dotnet/rules/architecture.md +55 -0
- okl/scaffold/profiles/dotnet/rules/messaging.md +31 -0
- okl/scaffold/profiles/dotnet/rules/performance-and-data.md +36 -0
- okl/scaffold/profiles/dotnet/rules/security.md +42 -0
- okl/scaffold/profiles/geospatial/README.md +6 -0
- okl/scaffold/profiles/geospatial/rules/geospatial-ml.md +38 -0
- okl/scaffold/profiles/python-rag/README.md +13 -0
- okl/scaffold/profiles/python-rag/rules/fastapi-backend.md +37 -0
- okl/scaffold/profiles/python-rag/rules/project-structure.md +28 -0
- okl/scaffold/profiles/python-rag/rules/rag-pipeline.md +73 -0
- okl/scaffold/profiles/react/README.md +18 -0
- okl/scaffold/profiles/react/rules/frontend.md +57 -0
- okl/scaffold/registries/RETRACTIONS.md +19 -0
- okl/scaffold/registries/tombstones.txt +7 -0
- okl/scaffold/root/CLAUDE.md +55 -0
- okl/scaffold/root/METHOD.md +64 -0
- okl/scaffold_cmd.py +110 -0
- okl/seed/dotnet-canon.json +489 -0
- okl/seed/dotnet-decisions.json +328 -0
- okl/seed/dotnet-defects.json +133 -0
- okl/seed/dotnet-review-surfaces.json +147 -0
- okl/seed/frontend-canon.json +116 -0
- okl/seed/geospatial-deeptime-defects.json +59 -0
- okl/seed/geospatial-defects.json +154 -0
- okl/seed/geospatial-enforcement-defects.json +121 -0
- okl/seed/geospatial-eval-defects.json +25 -0
- okl/seed/rag-defects.json +120 -0
- okl/seed/react-defects.json +45 -0
- okl/seed.py +55 -0
- okl/service.py +137 -0
- okl/store.py +432 -0
- org_knowledge_layer-0.1.0.dist-info/METADATA +475 -0
- org_knowledge_layer-0.1.0.dist-info/RECORD +67 -0
- org_knowledge_layer-0.1.0.dist-info/WHEEL +4 -0
- org_knowledge_layer-0.1.0.dist-info/entry_points.txt +2 -0
- org_knowledge_layer-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: React storefront canon — server state, effects, render/bundle perf, SPA auth
|
|
3
|
+
paths: ["frontend/**/*.tsx", "frontend/**/*.ts", "**/frontend/**/*.tsx", "**/frontend/**/*.ts"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React canon (frontend SPA)
|
|
7
|
+
|
|
8
|
+
> Portable React rules, ported verbatim from the .NET platform's `frontend/CLAUDE.md` (the .NET platform
|
|
9
|
+
> storefront) — but backend-agnostic: stack these onto ANY backend profile (dotnet, python-rag, …)
|
|
10
|
+
> whose repo has a React frontend. Reference stack: Vite + React 19 + TypeScript (strict), CSR SPA;
|
|
11
|
+
> TanStack Query v5 + Router; Zustand (small UI globals only); Tailwind v4 + shadcn/ui; oidc-client-ts
|
|
12
|
+
> → Keycloak (auth-code + PKCE); React Compiler on. Deep reference: the vendored
|
|
13
|
+
> `vercel-react-best-practices` skill (70 rules) — **but this canon wins where they disagree.**
|
|
14
|
+
>
|
|
15
|
+
> Where a rule cites "the backend" (VSA feature folders, server-controlled fields, cache-in-write-path,
|
|
16
|
+
> measure-before-optimizing), the parallel holds against whatever backend you pair this with.
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
- **Feature folders mirror the backend's VSA.** `src/features/<capability>/` owns its components, hooks, api calls, types. `src/shared/` is domain-agnostic; `src/core/` is singletons (query client, router, auth); `src/app/` is a thin shell — no business logic.
|
|
21
|
+
- **Feature boundaries are enforced, not conventional.** Features never import another feature's internals — only via its `index.ts` public API; `shared/` never imports from features. ESLint `import/no-restricted-paths` makes violations build errors.
|
|
22
|
+
- **Barrel files are intentional** — a feature's `index.ts` exports its public surface explicitly; everywhere else import directly (wildcard re-export barrels defeat tree-shaking).
|
|
23
|
+
- **Promotion to `shared/` requires proven reuse** (2–3 features independently need it). Speculative abstraction is the same dead weight as speculative interfaces in the backend.
|
|
24
|
+
|
|
25
|
+
## Server state (the biggest rule)
|
|
26
|
+
|
|
27
|
+
- **All server data flows through TanStack Query. Fetching in `useEffect` is banned** — hand-rolled effect-fetching badly reimplements dedup/caching/race-handling/retries the library gives you.
|
|
28
|
+
- **Query keys are a typed convention** `[feature, entity, params]` (e.g. `['catalog','products',{search}]`); key factories live in the feature's `api/` module.
|
|
29
|
+
- **Mutations invalidate (or update) their affected queries in the same mutation definition** (`onSuccess`), never "later" or via refetch-on-focus luck — the backend's "cache invalidation in the write path" rule, client-side.
|
|
30
|
+
- **Server data is never copied into `useState`** — render from the query result; copying makes a second source of truth that goes stale.
|
|
31
|
+
|
|
32
|
+
## Effects discipline ("You Might Not Need an Effect")
|
|
33
|
+
|
|
34
|
+
- **An Effect is only for synchronizing with an external system** (browser API, non-React widget, subscription). Everything else has a better tool: derived data → **calculate during render** (`useMemo` only if measured-expensive); reset-on-prop-change → **`key` prop**; "user did X" (POST/notify/navigate) → **event handler**, not an effect watching a flag; external store → **`useSyncExternalStore`**; state chains → compute in the handler; notify parent → call the callback in the handler or lift state up.
|
|
35
|
+
- **Effect dependencies are facts, not knobs.** Never lie to the dependency array to control *when* an effect runs — restructure instead. `eslint-plugin-react-hooks` `exhaustive-deps` is an error, not a warning.
|
|
36
|
+
|
|
37
|
+
## Render & bundle performance
|
|
38
|
+
|
|
39
|
+
- **Don't fight the React Compiler.** No reflexive `useMemo`/`useCallback`/`memo` — the compiler memoizes; manual memoization needs a profiler trace (the backend's "measure before optimizing"). Structural cases the compiler can't fix are still yours: don't define components inside components; hoist static JSX / default non-primitive props; functional `setState` for stable callbacks; refs for transient high-frequency values.
|
|
40
|
+
- **No request waterfalls on the critical path** — route loaders start queries before render; independent fetches run in parallel (`Promise.all` / parallel `useQuery`); Suspense streams what's ready. A fetch waiting on a render waiting on a fetch is the client-side N+1.
|
|
41
|
+
- **Route-level code splitting is the default**; heavy below-the-fold components via dynamic import; third-party scripts after hydration. **Initial bundle budget ≤ 200 KB gz, CI-checked.**
|
|
42
|
+
- **Virtualize lists past ~50 items.** **`startTransition`/`useDeferredValue` for non-urgent updates** (search-as-you-type) to keep input latency flat.
|
|
43
|
+
|
|
44
|
+
## Security
|
|
45
|
+
|
|
46
|
+
- **Auth flow: authorization code + PKCE, full stop.** The OAuth Browser-Based Apps BCP makes PKCE a MUST for SPA public clients and deprecates the implicit flow — no `response_type=token` anywhere.
|
|
47
|
+
- **Tokens live in memory (oidc-client-ts session), never `localStorage`** — XSS that reads storage steals tokens. **Documented trade-off:** the BCP ranks browser-held tokens as the least secure of its three patterns and recommends a BFF (tokens server-side, browser gets an HttpOnly cookie) for sensitive apps; acceptable here only because it's a demo storefront with fake data — if it ever fronts real user data, the BFF becomes the required shape.
|
|
48
|
+
- **Refresh tokens: rely on Keycloak's rotation** (BCP requires rotation/sender-constraining + bounded lifetime for SPA refresh tokens).
|
|
49
|
+
- **The client never computes or trusts money/authorization fields** — display what the server returns (backend's server-controlled-fields rule; the cart total is a preview, the authoritative total comes back from `POST /orders`).
|
|
50
|
+
- **No secrets in the bundle** — `import.meta.env` carries public config only.
|
|
51
|
+
|
|
52
|
+
## Testing & conventions
|
|
53
|
+
|
|
54
|
+
- **Test user-visible behavior, not implementation** — RTL queries by role/label; no testing hook internals or state shapes.
|
|
55
|
+
- **MSW mocks at the network boundary** — component/integration tests against mocked REST handlers per service (including error and slow cases).
|
|
56
|
+
- **Every feature ships happy path + error path + loading/empty state tests.** **Playwright E2E for the saga walk-through** runs against the real Aspire stack — regression gate and demo script both.
|
|
57
|
+
- Components `PascalCase.tsx`, hooks `useX.ts`, feature folders `kebab-case`, one component per file, named exports (default only where the router requires). TypeScript strict; no `any` (`unknown` + narrowing); API response types derived from backend DTO shapes.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Retractions
|
|
2
|
+
|
|
3
|
+
Claims this project once made and has since found to be false. A retraction is a **receipt**, not a
|
|
4
|
+
deletion — the wrong claim stays visible with its correction so it cannot be quietly restated.
|
|
5
|
+
|
|
6
|
+
The `retractions` gate fails any tracked doc that states a retracted claim without also retracting it.
|
|
7
|
+
|
|
8
|
+
## Format
|
|
9
|
+
Each entry: a stable `id`, the retracted claim (quoted), why it is false, and the date/commit.
|
|
10
|
+
|
|
11
|
+
<!-- <<FILL: retractions as you earn them. Example from the RAG service: -->
|
|
12
|
+
<!--
|
|
13
|
+
### R-0001 — "agentic's retrieval is architecturally weaker than classic"
|
|
14
|
+
- **Retracted:** the conclusion in `docs/rag-mode-eval-results.md`.
|
|
15
|
+
- **Why false:** the agent's primary tool (`search_filtered`) threw on every call and it was given
|
|
16
|
+
top_k=3 vs classic's 8. It measured broken instrumentation, not an architecture. Error-analysis
|
|
17
|
+
cross-tab showed generation, not retrieval, is the dominant failure mode.
|
|
18
|
+
- **Date/commit:** 2026-07 / findings-log Part 2.
|
|
19
|
+
-->
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Tombstones — retired identifiers that must not be resurrected.
|
|
2
|
+
# One identifier per line. The `tombstones` gate fails any doc/comment/config that reintroduces one.
|
|
3
|
+
# Format: <identifier> <TAB> <reason / what replaced it> <TAB> <date>
|
|
4
|
+
#
|
|
5
|
+
# <<FILL as you retire things. Examples:>>
|
|
6
|
+
# python-rag-service_full empty-entity stray collection; canonical is python-rag-service_hybrid 2026-07
|
|
7
|
+
# python-rag-service_ids stray collection; deleted in 36db337 2026-07
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# {{REPO}} — agent instructions (canon, surface 2)
|
|
2
|
+
|
|
3
|
+
> New session? Read `.claude/rules/` (loaded by path) and run `/feature-spec` before non-trivial work.
|
|
4
|
+
> This file is the **always-on canon**. Keep it lean — target ~60–150 lines. Detail lives in
|
|
5
|
+
> `.claude/rules/*.md` (path-scoped), `.claude/skills/` (on-demand), and `docs/` (deep reference).
|
|
6
|
+
> CI enforces a size budget (warn 200 / fail 300 lines). This is surface 2 of the encoding loop.
|
|
7
|
+
|
|
8
|
+
## The method (portable — do not delete)
|
|
9
|
+
|
|
10
|
+
This repo runs the **encoding loop**: a trigger (planning, a bug, a review finding, an audit)
|
|
11
|
+
produces a rule; the response is always **pick the smallest surface → encode it → promote down the
|
|
12
|
+
spectrum as it earns its keep**. Five surfaces, softest→strongest (1→5 *is* moving down):
|
|
13
|
+
|
|
14
|
+
1. Deep reference — `docs/` + diagrams (passive)
|
|
15
|
+
2. Always-on canon — this file + `.claude/`
|
|
16
|
+
3. Procedure rituals — `.claude/skills/` + `.claude/commands/` (user-invoked)
|
|
17
|
+
4. PR-review automation — `.coderabbit.yaml` + `.claude/agents/architecture-reviewer.md`
|
|
18
|
+
5. Mechanical gates — CI, tests, hooks (build fails if violated)
|
|
19
|
+
|
|
20
|
+
**Before starting work, the pre-task hook injects the org's relevant lessons** via `okl check`
|
|
21
|
+
(surface 6 — the cross-repo layer). Read them. Retractions, tombstones, and armed gates in that
|
|
22
|
+
briefing are binding. If it says a claim is retracted, do not restate it as fact.
|
|
23
|
+
|
|
24
|
+
**When you learn something worth keeping** ("never write this again" / "always do this when"),
|
|
25
|
+
encode it the same session at the smallest sufficient surface, and `okl record` it — `scope=org`
|
|
26
|
+
if it is a fact about the world (prior art, an API contract, a data gotcha), `scope=repo` if it is
|
|
27
|
+
a quirk of this codebase only. A merged fix without the encoded rule is a half-finished job.
|
|
28
|
+
|
|
29
|
+
## Always-on rules (keep few; most rules belong in `.claude/rules/` or skills)
|
|
30
|
+
|
|
31
|
+
- Never commit secrets, `.env` files, or credentials.
|
|
32
|
+
- Every non-trivial change starts with `/feature-spec` (value gate + significance check).
|
|
33
|
+
- Any spec/plan/scaffold **assertion** ("these paths are correct", "this config is valid") earns a
|
|
34
|
+
mechanical `validate` step before it is trusted — never assert from memory. (SDD's one load-bearing rung.)
|
|
35
|
+
- Report the result that came out, especially when it disproves your own hypothesis.
|
|
36
|
+
- Verify a gate by making it FAIL on real drift before trusting it to pass.
|
|
37
|
+
|
|
38
|
+
<!-- <<FILL: STACK-SPECIFIC ALWAYS-ON RULES>>
|
|
39
|
+
Add the handful of rules every session in THIS repo needs (language, framework, house style).
|
|
40
|
+
Keep each to one bolded headline + one paragraph; move the rationale to docs/ or a rule file.
|
|
41
|
+
Delete this comment when filled. Examples of what belongs here vs. a path-scoped rule file:
|
|
42
|
+
- belongs here: "All money math is server-computed, never trusted from the client."
|
|
43
|
+
- belongs in .claude/rules/db.md (paths: **/*.sql): EF/query conventions.
|
|
44
|
+
-->
|
|
45
|
+
|
|
46
|
+
## Build & test
|
|
47
|
+
|
|
48
|
+
<!-- <<FILL: BUILD/TEST COMMANDS>> e.g. `make test`, `dotnet build`, `pytest -q`, `npm run ci` -->
|
|
49
|
+
|
|
50
|
+
## Encoding surfaces in this repo (auto-maintained)
|
|
51
|
+
|
|
52
|
+
- Registries: `registries/RETRACTIONS.md`, `registries/tombstones.txt`
|
|
53
|
+
- Gates: `gates/` (run via `gates/run-gates.sh`; required in CI)
|
|
54
|
+
- Evals: `evals/` (golden set from real failures; gates on measured behavior)
|
|
55
|
+
- Knowledge layer: `okl check` / `okl record` (see `.okl/config.json`)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# The Method — portable engineering discipline (the encoding loop)
|
|
2
|
+
|
|
3
|
+
> This is the method in its entirety, distilled from three repositories that run it across three
|
|
4
|
+
> different stacks: **the .NET platform** (.NET microservices), **the geospatial pipeline** (geospatial /
|
|
5
|
+
> rslearn / OlmoEarth), and **the RAG service** (Python / FastAPI / RAG). A rule is in this file — the
|
|
6
|
+
> *portable* skeleton — only if it showed up in more than one of them independently. Stack-specific
|
|
7
|
+
> rules live in `.claude/rules/*.md` and the `<<FILL>>` slots, not here.
|
|
8
|
+
|
|
9
|
+
## The core (the whole method in four sentences)
|
|
10
|
+
|
|
11
|
+
1. **AI-assisted work fails by producing output that is fast, fluent, plausible, and wrong.** Every
|
|
12
|
+
rule merely *documented* drifts; every rule *mechanized* holds. So the method is not exhortation —
|
|
13
|
+
it is surfaces that make the right thing automatic and the wrong thing fail loudly.
|
|
14
|
+
2. **The encoding loop:** a trigger (planning, a bug, a review finding, an audit, an incident)
|
|
15
|
+
surfaces a rule. The response is always *pick the smallest sufficient surface → encode it →
|
|
16
|
+
promote it down the spectrum (toward mechanical) only as it earns its keep.* It is a ratchet: it
|
|
17
|
+
does not slip back.
|
|
18
|
+
3. **Six surfaces, softest→strongest:** (1) deep reference `docs/`, (2) always-on canon `CLAUDE.md`
|
|
19
|
+
+ `.claude/rules/`, (3) procedure rituals `.claude/skills/` + commands, (4) PR-review automation
|
|
20
|
+
`.coderabbit.yaml` + `architecture-reviewer`, (5) mechanical gates CI/tests/hooks, (6) the
|
|
21
|
+
cross-repo org knowledge layer (`okl`). 1–2 are Tier 1, 3–4 Tier 2, 5 Tier 3; 6 spans repos.
|
|
22
|
+
4. **A surface nobody runs is documentation, not enforcement.** If a rule is not read or executed on
|
|
23
|
+
the path where it matters, it will drift — so the method's own defects are dated, receipted, and
|
|
24
|
+
registered, and the gates are verified by making them *fail* on real drift before trusting them.
|
|
25
|
+
|
|
26
|
+
## The seven portable rules (each earned in ≥2 of the three repos)
|
|
27
|
+
|
|
28
|
+
1. **Don't ask a model to infer what you can look up.** Assertions written from memory (class paths,
|
|
29
|
+
entity identities, config validity) are the single most common defect class across all three
|
|
30
|
+
repos. Every such assertion earns a mechanical `validate` step. *Let the model choose the
|
|
31
|
+
constraint; make code satisfy it.*
|
|
32
|
+
2. **A signal that cannot report its own failure is not a signal.** An exit code of 0 with zero
|
|
33
|
+
outputs; a judge that averages only the runs that didn't crash; a "PASS" with no fixture behind
|
|
34
|
+
it. Every gate and every metric must be able to say when it is unreliable, and say it first.
|
|
35
|
+
3. **Verify the layer before you build on it.** Confirm the thing you depend on actually works —
|
|
36
|
+
with a real, non-trivial input — before you stack more on top. (A graph planned on a 0%-populated
|
|
37
|
+
entity layer; a model trained on a 0-file materialized dataset.)
|
|
38
|
+
4. **Fixtures you invented cannot falsify assumptions you hold.** Test against real data sampled from
|
|
39
|
+
the actual corpus/inputs, not fixtures written by the same person who wrote the code under test.
|
|
40
|
+
The adversarial audit (`/paper-audit`) exists to attack your own confident claims.
|
|
41
|
+
5. **Report the result that came out** — especially when it disproves your hypothesis. When a result
|
|
42
|
+
turns out wrong, *retract it in a registry*, don't quietly delete it. The retraction is a receipt.
|
|
43
|
+
6. **Record decisions, including the ones you rejected and why.** "Options considered and rejected"
|
|
44
|
+
is as valuable as what you built. An ADR is append-only: supersede, never edit-to-erase.
|
|
45
|
+
7. **Use the instrumentation you already have.** The trace, the metric, the log, the cross-tab —
|
|
46
|
+
build the observability once and then actually read it, instead of debugging by `grep`.
|
|
47
|
+
|
|
48
|
+
## What is deliberately NOT in this file (stack-specific — goes in FILL slots / rule files)
|
|
49
|
+
|
|
50
|
+
- Framework rules (VSA vs Clean, Wolverine handler discovery, EF outbox) → the .NET platform's canon.
|
|
51
|
+
- Domain rules (spatial cross-validation, class-scheme crosswalks, GDAL temp dirs) → the geospatial repo.
|
|
52
|
+
- Pipeline rules (reranker config, Qdrant pre-filter, corpus-aggregate vs ranked retrieval) → the RAG service.
|
|
53
|
+
|
|
54
|
+
Each of those is *real method* — but it is fill, not skeleton. The kit ships the slots; you drop the
|
|
55
|
+
stack rules in per repo (`.claude/rules/<area>.md` with a `paths:` glob), and `okl record --scope repo`
|
|
56
|
+
them so they are enforced without polluting another repo's `okl check`.
|
|
57
|
+
|
|
58
|
+
## Sources
|
|
59
|
+
|
|
60
|
+
Ported verbatim from the three repositories' own method documents:
|
|
61
|
+
`the geospatial pipeline/docs/method.md`; the .NET platform `CLAUDE.md` + `CONTEXT.md` + `.github/AI_WORKFLOW.md`;
|
|
62
|
+
the RAG service `docs/agent-contract.md` + `docs/findings-log.md`. Current-tool practices (Claude Code
|
|
63
|
+
plugins, subagent memory frontmatter, `.claude/rules` path-scoping, hook exit-code-2 blocking) verified
|
|
64
|
+
against the official Claude Code docs.
|
okl/scaffold_cmd.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""`okl scaffold` — stamp the portable method kit into a repo.
|
|
2
|
+
|
|
3
|
+
Copies the template tree shipped inside the package (src/okl/scaffold/) into the target repo,
|
|
4
|
+
renaming `claude/` -> `.claude/` (the package can't ship a dotfile dir on some filesystems), and
|
|
5
|
+
substituting `{{REPO}}`. Never overwrites an existing file unless --force; prints a FILL worklist.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
SCAFFOLD_ROOT = Path(__file__).parent / "scaffold"
|
|
13
|
+
|
|
14
|
+
# (src relative to scaffold/, dst relative to repo root). `{c}` is the claude dir name
|
|
15
|
+
# (".claude" in real use; overridable only so the logic is testable inside sandboxes that
|
|
16
|
+
# forbid creating a literal ".claude" path).
|
|
17
|
+
def _layout(c: str = ".claude"):
|
|
18
|
+
return [
|
|
19
|
+
("root/CLAUDE.md", "CLAUDE.md"),
|
|
20
|
+
("root/CLAUDE.md", "AGENTS.md"), # same source, two names: Claude Code + every other agent
|
|
21
|
+
("root/METHOD.md", "METHOD.md"),
|
|
22
|
+
("claude", c), # dir: skills/agents/commands/rules
|
|
23
|
+
("gates", "gates"),
|
|
24
|
+
("registries", "registries"),
|
|
25
|
+
("evals", "evals"),
|
|
26
|
+
("ci/method-gates.yml", ".github/workflows/method-gates.yml"),
|
|
27
|
+
("ci/okl-verify.yml", ".github/workflows/okl-verify.yml"),
|
|
28
|
+
("hooks/userpromptsubmit-okl-check.sh", f"{c}/hooks/userpromptsubmit-okl-check.sh"),
|
|
29
|
+
("hooks/stop-okl-encode.sh", f"{c}/hooks/stop-okl-encode.sh"),
|
|
30
|
+
("hooks/hooks.json", f"{c}/hooks/hooks.json"),
|
|
31
|
+
("MANIFEST.md", "docs/method-kit-manifest.md"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
PLUGIN_LAYOUT = [
|
|
35
|
+
("plugin/plugin.json", "plugin.json"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _copy(src: Path, dst: Path, repo: str, force: bool, written: list, skipped: list):
|
|
40
|
+
if src.is_dir():
|
|
41
|
+
for child in src.rglob("*"):
|
|
42
|
+
if child.is_file():
|
|
43
|
+
rel = child.relative_to(src)
|
|
44
|
+
_copy_file(child, dst / rel, repo, force, written, skipped)
|
|
45
|
+
else:
|
|
46
|
+
_copy_file(src, dst, repo, force, written, skipped)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _copy_file(src: Path, dst: Path, repo: str, force: bool, written: list, skipped: list):
|
|
50
|
+
if dst.exists() and not force:
|
|
51
|
+
skipped.append(dst)
|
|
52
|
+
return
|
|
53
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
text = None
|
|
55
|
+
try:
|
|
56
|
+
text = src.read_text()
|
|
57
|
+
except (UnicodeDecodeError, ValueError):
|
|
58
|
+
shutil.copy2(src, dst) # binary
|
|
59
|
+
written.append(dst)
|
|
60
|
+
return
|
|
61
|
+
dst.write_text(text.replace("{{REPO}}", repo))
|
|
62
|
+
if dst.suffix == ".sh":
|
|
63
|
+
dst.chmod(0o755)
|
|
64
|
+
written.append(dst)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def list_profiles() -> list[str]:
|
|
68
|
+
d = SCAFFOLD_ROOT / "profiles"
|
|
69
|
+
return sorted(p.name for p in d.iterdir() if p.is_dir()) if d.is_dir() else []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def scaffold(target: str = ".", repo: str | None = None, force: bool = False,
|
|
73
|
+
plugin: bool = False, claude_dir: str = ".claude",
|
|
74
|
+
profile: str | list[str] | None = None) -> dict:
|
|
75
|
+
root = Path(target).resolve()
|
|
76
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
repo = repo or root.name
|
|
78
|
+
written: list[Path] = []
|
|
79
|
+
skipped: list[Path] = []
|
|
80
|
+
|
|
81
|
+
# Profiles are composable — stack a backend profile with react, etc.
|
|
82
|
+
profiles = [profile] if isinstance(profile, str) else list(profile or [])
|
|
83
|
+
if profiles:
|
|
84
|
+
avail = list_profiles()
|
|
85
|
+
for p in profiles:
|
|
86
|
+
if p not in avail:
|
|
87
|
+
raise ValueError(f"unknown profile {p!r}; available: {', '.join(avail) or '(none)'}")
|
|
88
|
+
|
|
89
|
+
layout = _layout(claude_dir) + (PLUGIN_LAYOUT if plugin else [])
|
|
90
|
+
for src_rel, dst_rel in layout:
|
|
91
|
+
_copy(SCAFFOLD_ROOT / src_rel, root / dst_rel, repo, force, written, skipped)
|
|
92
|
+
|
|
93
|
+
# Each profile drops its stack's verbatim canon into .claude/rules/ (+ a README).
|
|
94
|
+
for p in profiles:
|
|
95
|
+
prof_root = SCAFFOLD_ROOT / "profiles" / p
|
|
96
|
+
_copy(prof_root / "rules", root / claude_dir / "rules", repo, force, written, skipped)
|
|
97
|
+
_copy(prof_root / "README.md", root / claude_dir / "rules" / f"_PROFILE_{p}.md",
|
|
98
|
+
repo, force, written, skipped)
|
|
99
|
+
|
|
100
|
+
# find FILL slots across everything just written
|
|
101
|
+
fills = []
|
|
102
|
+
for p in written:
|
|
103
|
+
try:
|
|
104
|
+
for i, line in enumerate(p.read_text().splitlines(), 1):
|
|
105
|
+
if "<<FILL" in line:
|
|
106
|
+
fills.append(f"{p.relative_to(root)}:{i}")
|
|
107
|
+
except (UnicodeDecodeError, ValueError):
|
|
108
|
+
pass
|
|
109
|
+
return {"repo": repo, "root": str(root), "written": [str(p.relative_to(root)) for p in written],
|
|
110
|
+
"skipped": [str(p.relative_to(root)) for p in skipped], "fills": fills, "plugin": plugin}
|