@cratis/pi 0.0.1 → 2.0.1
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/LICENSE +21 -0
- package/README.md +18 -37
- package/package/corpus/agents/backend-developer.md +125 -0
- package/package/corpus/agents/code-reviewer.md +165 -0
- package/package/corpus/agents/coordinator.md +163 -0
- package/package/corpus/agents/frontend-developer.md +246 -0
- package/package/corpus/agents/orchestrator.md +196 -0
- package/package/corpus/agents/performance-reviewer.md +109 -0
- package/package/corpus/agents/planner.md +145 -0
- package/package/corpus/agents/repository-investigation-reviewer.md +45 -0
- package/package/corpus/agents/repository-investigator.md +50 -0
- package/package/corpus/agents/security-reviewer.md +118 -0
- package/package/corpus/agents/slice-implementer.md +59 -0
- package/package/corpus/agents/spec-writer.md +149 -0
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +213 -0
- package/package/corpus/harnesses/pi/extensions/cratis-rules/index.ts +27 -0
- package/package/corpus/harnesses/pi/extensions/package.json +4 -0
- package/package/corpus/harnesses/pi/extensions/subagent/agents.ts +167 -0
- package/package/corpus/harnesses/pi/extensions/subagent/index.ts +352 -0
- package/package/corpus/hooks/README.md +434 -0
- package/package/corpus/hooks/agent-stop.md +49 -0
- package/package/corpus/hooks/pre-commit.md +47 -0
- package/package/corpus/hooks/scripts/cratis-guard-writes.sh +87 -0
- package/package/corpus/hooks/scripts/cratis-nuget-pins.txt +11 -0
- package/package/corpus/hooks/scripts/cratis-pattern-scan.sh +197 -0
- package/package/corpus/hooks/scripts/cratis-patterns.json +84 -0
- package/package/corpus/hooks/scripts/cratis-quality-gate.sh +219 -0
- package/package/corpus/hooks/scripts/hook-lib.sh +152 -0
- package/package/corpus/hooks/scripts/quality-gates.json +261 -0
- package/package/corpus/hooks/scripts/type-references-allowlist.txt +71 -0
- package/package/corpus/hooks/scripts/validate-package-imports.sh +166 -0
- package/package/corpus/hooks/scripts/validate-package-subpaths.sh +120 -0
- package/package/corpus/hooks/scripts/validate-type-references.sh +308 -0
- package/package/corpus/hooks/settings.template.json +40 -0
- package/package/corpus/prompts/add-business-rule.prompt.md +22 -0
- package/package/corpus/prompts/add-concept.prompt.md +17 -0
- package/package/corpus/prompts/add-ef-migration.prompt.md +24 -0
- package/package/corpus/prompts/add-projection.prompt.md +20 -0
- package/package/corpus/prompts/add-reactor.prompt.md +22 -0
- package/package/corpus/prompts/add-reducer.prompt.md +20 -0
- package/package/corpus/prompts/audit-hooks.prompt.md +15 -0
- package/package/corpus/prompts/check-doc-drift.prompt.md +21 -0
- package/package/corpus/prompts/code-review.prompt.md +9 -0
- package/package/corpus/prompts/new-feature.prompt.md +9 -0
- package/package/corpus/prompts/new-vertical-slice.prompt.md +18 -0
- package/package/corpus/prompts/review-pr.prompt.md +35 -0
- package/package/corpus/prompts/review-skill.prompt.md +16 -0
- package/package/corpus/prompts/scaffold-feature.prompt.md +16 -0
- package/package/corpus/prompts/ship-changes.prompt.md +20 -0
- package/package/corpus/prompts/verify-ai-setup.prompt.md +19 -0
- package/package/corpus/prompts/write-documentation.prompt.md +21 -0
- package/package/corpus/prompts/write-specs.prompt.md +22 -0
- package/package/corpus/rules/capability-is-not-authority.md +31 -0
- package/package/corpus/rules/code-quality.csharp.md +91 -0
- package/package/corpus/rules/code-quality.md +82 -0
- package/package/corpus/rules/code-quality.typescript.md +89 -0
- package/package/corpus/rules/components.md +207 -0
- package/package/corpus/rules/concepts.md +115 -0
- package/package/corpus/rules/csharp.md +269 -0
- package/package/corpus/rules/dialogs.md +264 -0
- package/package/corpus/rules/documentation-structure-and-formatting.md +148 -0
- package/package/corpus/rules/documentation.md +90 -0
- package/package/corpus/rules/editing-cratis-docs.md +69 -0
- package/package/corpus/rules/efcore.md +235 -0
- package/package/corpus/rules/efcore.specs.md +44 -0
- package/package/corpus/rules/exit-codes-and-wrappers.md +33 -0
- package/package/corpus/rules/framework.md +52 -0
- package/package/corpus/rules/frontend-quality.md +59 -0
- package/package/corpus/rules/frontend-testing.md +126 -0
- package/package/corpus/rules/general.md +305 -0
- package/package/corpus/rules/git-commits.md +138 -0
- package/package/corpus/rules/github-actions.md +92 -0
- package/package/corpus/rules/glossary.md +61 -0
- package/package/corpus/rules/guards-and-fuses.md +45 -0
- package/package/corpus/rules/local-work-artifacts.md +33 -0
- package/package/corpus/rules/managing-ai-rules.md +40 -0
- package/package/corpus/rules/orleans.md +50 -0
- package/package/corpus/rules/pull-requests.md +78 -0
- package/package/corpus/rules/react.md +195 -0
- package/package/corpus/rules/reactors.md +238 -0
- package/package/corpus/rules/rtk.md +37 -0
- package/package/corpus/rules/specs.csharp.md +139 -0
- package/package/corpus/rules/specs.md +132 -0
- package/package/corpus/rules/specs.scenarios.csharp.md +172 -0
- package/package/corpus/rules/specs.typescript.md +139 -0
- package/package/corpus/rules/storybook.md +84 -0
- package/package/corpus/rules/terminal-commands.md +19 -0
- package/package/corpus/rules/typescript.md +149 -0
- package/package/corpus/rules/verification-discipline.md +21 -0
- package/package/corpus/rules/vertical-slices.md +338 -0
- package/package/corpus/rules/web-fetching.md +11 -0
- package/package/corpus/rules/writing-correct-examples.md +35 -0
- package/package/corpus/rules/writing-cratis-docs.md +70 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/SKILL.md +131 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/code-style.md +187 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/domain-philosophy.md +91 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/exceptions-logging-and-di.md +223 -0
- package/package/corpus/skills/cratis-engineering-decision-record/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-decision-record/SKILL.md +133 -0
- package/package/corpus/skills/cratis-engineering-decision-record/references/record-format.md +107 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/SKILL.md +86 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/references/site-format.md +46 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/SKILL.md +130 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/references/failure-archetypes.md +133 -0
- package/package/corpus/skills/cratis-fundamentals-concept/verification.json +8 -0
- package/package/profile-catalog.json +821 -0
- package/package.json +35 -13
- package/src/index.ts +80 -0
- /package/{skills → package/corpus/skills}/cratis-application-react-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-react-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authentication.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authorization.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/frontend.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/local-development.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/command-result.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/handler-shapes.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/proxy-generation.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/read-model-injection.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-execution/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-execution/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-validation/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-validation/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-query-paging/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-query-paging/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-page.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-tables.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/dialogs.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/mvvm.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/queries-and-commands.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/blocked-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/observational-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/fluent-builder.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/model-bound-attributes.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/references/queries.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-code-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-code-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-accessibility/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-accessibility/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-schema-editor/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-schema-editor/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-styling/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-styling/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-toolbar/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-toolbar/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-documentation-writing/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-documentation-writing/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-event-model-diagram/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-event-model-diagram/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-performance-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-performance-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-security-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-security-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specification-by-example/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specification-by-example/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/application-scenarios.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/csharp-patterns.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/integration-specs.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/references/typescript-patterns.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/observational-tools.md +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# Hooks — enforcement, not persuasion
|
|
2
|
+
|
|
3
|
+
Everything else in `.cratis/ai/` is text an agent may or may not follow. The files here are the part
|
|
4
|
+
that runs. They convert the mechanically-checkable Cratis invariants into deterministic checks
|
|
5
|
+
that fire whether or not the model remembered the rule.
|
|
6
|
+
|
|
7
|
+
Three layers:
|
|
8
|
+
|
|
9
|
+
| Layer | Event | Script | Cost | Effect |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| Pattern pass | `PostToolUse` on a write | `scripts/cratis-pattern-scan.sh` | zero tokens until a match | appends a one-line reminder to context, never blocks |
|
|
12
|
+
| Hard block | `PreToolUse` on a write | `scripts/cratis-guard-writes.sh` | zero | exits **2** — the write does not happen |
|
|
13
|
+
| Quality gate | `Stop` | `scripts/cratis-quality-gate.sh` | one build/test run, only when relevant files changed | exits **2** — the turn does not end |
|
|
14
|
+
|
|
15
|
+
The Claude Code wiring that fires them is tracked here, in
|
|
16
|
+
[`settings.template.json`](./settings.template.json). Claude reads `.claude/settings.json`, which is
|
|
17
|
+
per-machine and gitignored, so activate the hooks by copying the template once:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cp .cratis/ai/hooks/settings.template.json .claude/settings.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If you already have a `.claude/settings.json`, merge the template's `hooks` block into it rather
|
|
24
|
+
than overwriting — the rest of that file is yours. Re-copy after the template changes; the copy is
|
|
25
|
+
not a symlink, so it does not update itself. **Edit the template, never the copy**: `.cratis/ai/` is the
|
|
26
|
+
source of truth (see [`../rules/managing-ai-rules.md`](../rules/managing-ai-rules.md)), and
|
|
27
|
+
`scripts/validate-ai-setup.sh` checks the template against the script names this page documents.
|
|
28
|
+
|
|
29
|
+
The markdown files in this folder (`agent-stop.md`, `pre-commit.md`) remain *lifecycle guidance* —
|
|
30
|
+
they describe what a hook should do for tools that have no wiring yet.
|
|
31
|
+
|
|
32
|
+
> Hooks are the one surface with no folder adapter: Claude reads `.claude/settings.json`,
|
|
33
|
+
> Copilot would read `.github/hooks/*.json`. Only the Claude wiring exists today.
|
|
34
|
+
|
|
35
|
+
## What is enforced
|
|
36
|
+
|
|
37
|
+
Rule numbers refer to the numbered list in [`../rules/general.md`](../rules/general.md).
|
|
38
|
+
|
|
39
|
+
**Blocked outright** (`PreToolUse`, exit 2):
|
|
40
|
+
|
|
41
|
+
- Editing a file whose header marks it as Cratis-generated output — rule 15 `[contract]`
|
|
42
|
+
- Writing content that opens with such a header (hand-authoring a "generated" proxy)
|
|
43
|
+
- `Directory.Packages.props`, `global.json`, `NuGet.config`, `yarn.lock`, `package-lock.json`,
|
|
44
|
+
`pnpm-lock.yaml`, `packages.lock.json` — the Source-of-Truth Discipline rule
|
|
45
|
+
- `.env`, `.env.*`, `*.env` — secrets
|
|
46
|
+
|
|
47
|
+
The generated-file check is anchored: the marker must be a comment opener at the start of one of
|
|
48
|
+
the first five lines. A rule file or a document that merely *mentions* the marker is not blocked.
|
|
49
|
+
|
|
50
|
+
**Flagged** (`PostToolUse`, exit 0 + context):
|
|
51
|
+
|
|
52
|
+
| Pattern id | Rule | Detects |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `cratis-automap-call` | 10 `[contract]` | `.AutoMap()` in a file that never calls `.NoAutoMap()` |
|
|
55
|
+
| `cratis-ieventlog-in-handle` | 14 `[contract]` | `IEventLog` in a `Handle(` signature, wrapping across up to 5 lines |
|
|
56
|
+
| `cratis-nullable-event-property` | 6 `[contract]` | a nullable property inside a type declared with `[EventType]` |
|
|
57
|
+
| `cratis-route-on-readmodel` | 12 `[contract]` | `[Route(` inside a type declared with `[ReadModel]` |
|
|
58
|
+
| `cratis-controller-base` | 1 `[contract]` | `: ControllerBase` in a file that imports `Microsoft.AspNetCore.Mvc` |
|
|
59
|
+
| `cratis-primereact-dialog-import` | 16 `[convention]` | `from 'primereact/dialog'` |
|
|
60
|
+
|
|
61
|
+
The two `within_type_attribute` patterns are not line greps — the scanner tracks C# attribute
|
|
62
|
+
blocks and type scope (positional record, multi-line declaration, or braced body), so a nullable
|
|
63
|
+
property is only reported when it really sits inside an `[EventType]`.
|
|
64
|
+
|
|
65
|
+
**Gated** (`Stop`, exit 2): the app-pinned commands from the Quality Gates table in
|
|
66
|
+
`general.md` and the steps in [`agent-stop.md`](./agent-stop.md) — Debug build, specs, Release
|
|
67
|
+
build (with `-p:CratisProxiesOutputPath=` per `general.md`, so the proxy generator does not
|
|
68
|
+
re-run and touch already-correct generated files), frontend lint / compile / compile-specs /
|
|
69
|
+
test, and `validate-ai-setup.sh` for corpus changes.
|
|
70
|
+
|
|
71
|
+
## The corpus validator
|
|
72
|
+
|
|
73
|
+
`scripts/validate-ai-setup.sh` sits outside the three layers: it validates `.cratis/ai/` itself, and both
|
|
74
|
+
the `Stop` gate and the `ai-corpus` CI job run it. Structural, adapter and Codex checks are
|
|
75
|
+
**fatal**; the content drift guards **warn**.
|
|
76
|
+
|
|
77
|
+
### Package subpath existence — `scripts/validate-package-subpaths.sh` (warn)
|
|
78
|
+
|
|
79
|
+
Every other drift guard asserts that a string should *not* appear. This one is the other direction,
|
|
80
|
+
and the only guard that knows what a package is. It extracts each `@cratis/<pkg>/<subpath>` the
|
|
81
|
+
corpus names — fenced blocks, inline spans and table cells alike — from `.cratis/ai/rules`, `.cratis/ai/skills`,
|
|
82
|
+
`.cratis/ai/agents` and `.cratis/ai/prompts`, then resolves it against the `exports` map of the package installed
|
|
83
|
+
in `node_modules`. The exports map is exact and machine-readable, so a miss is a genuine miss.
|
|
84
|
+
`.cratis/ai/hooks` is deliberately *not* one of the default roots — this page names bogus subpaths as
|
|
85
|
+
examples, and a guard that reports its own documentation is a guard people switch off.
|
|
86
|
+
|
|
87
|
+
It exists because nothing in the repository could catch documenting
|
|
88
|
+
`@cratis/components/Notifications` (a subpath that first ships in **3.0.0**) while the pin is
|
|
89
|
+
**2.6.1**. A prose-pattern matcher has no notion of a package, a version, or an exports map; a
|
|
90
|
+
developer following the corpus got a module-resolution failure.
|
|
91
|
+
|
|
92
|
+
**Warn, never fail — the tradeoff.** The observation is exact but the conclusion is not: "the corpus
|
|
93
|
+
names an API that does not exist" and "this repository is pinned behind the version the corpus
|
|
94
|
+
documents" look identical from the exports map. This script propagates to every Cratis repository,
|
|
95
|
+
and the `ai-corpus` CI job checks out the tree and installs nothing — so failing would be a
|
|
96
|
+
permanent no-op in CI while turning repos red locally for their own dependency pin. The warning
|
|
97
|
+
names the file, the line and the installed version, and leaves the judgement to a human.
|
|
98
|
+
|
|
99
|
+
> **What this repository is.** `Cratis/AI` is a corpus of markdown, JSON and a little
|
|
100
|
+
> JavaScript — it has no `Source/`, no `.slnx`, no `package.json` and no C# or TypeScript
|
|
101
|
+
> project of its own. Every `.cs` / `.ts` / `Source/**` reference below describes what the
|
|
102
|
+
> hooks do in a **consuming** repository. Here they are silent, which is the designed
|
|
103
|
+
> behavior, not a broken setup.
|
|
104
|
+
|
|
105
|
+
**Silent when it cannot judge.** No `jq`, no `node_modules`, a package this repository does not
|
|
106
|
+
depend on, or a package published without an `exports` map: skipped without a word. "Not installed"
|
|
107
|
+
is not a finding.
|
|
108
|
+
|
|
109
|
+
**Version-qualified lines are not drift.** The corpus deliberately documents some 3.0.0+ APIs
|
|
110
|
+
against a 2.x pin, marked inline as `(**≥ 3.0.0**)`. A reference is cleared when a line mentioning
|
|
111
|
+
it in the same file also carries a version — a dotted number, an `N.x`, or either inequality
|
|
112
|
+
spelling. Qualification is judged per *(file, reference)* rather than per line, because the corpus
|
|
113
|
+
states a requirement once and then mentions the subpath again unqualified nearby; per-line matching
|
|
114
|
+
would fire on exactly the lines someone had just fixed correctly. The check is deliberately generous
|
|
115
|
+
in the same direction: it would rather miss a stale line than warn about a correct one.
|
|
116
|
+
|
|
117
|
+
**What it deliberately does not check.** Named imports (`import { Toaster } from '…'`) are Tier 2's
|
|
118
|
+
job, below; .NET types named in prose or in a C# type position are Tier 3's. This tier checks module
|
|
119
|
+
specifiers, nothing else.
|
|
120
|
+
|
|
121
|
+
Run it standalone, optionally over other roots, and add `CRATIS_HOOKS_SUBPATH_REPORT=1` to see every
|
|
122
|
+
reference and how it resolved rather than only the failures. It invokes Tier 3 before its own gates
|
|
123
|
+
and Tier 2 after its own work, over the same roots, so the single call site in
|
|
124
|
+
`validate-ai-setup.sh` gets all three.
|
|
125
|
+
|
|
126
|
+
### Named import existence — `scripts/validate-package-imports.sh` (warn)
|
|
127
|
+
|
|
128
|
+
Tier 2, and the reason it exists is that Tier 1's answer is not the whole question: a subpath that
|
|
129
|
+
resolves says nothing about the *names* imported through it. For every
|
|
130
|
+
`import { A, B } from '@cratis/<pkg>/<subpath>'` in the corpus — single-line, brace-on-its-own-line,
|
|
131
|
+
`import type`, `A as B` (the *imported* name is what has to exist), trailing `//` comments — it
|
|
132
|
+
checks each identifier against the `.d.ts` closure of the installed package and warns about the ones
|
|
133
|
+
that are not there. `Toaster`, `toastCommandResult`, `PasswordField`, `RatingField` and the rest are
|
|
134
|
+
real APIs of `@cratis/components` **3.0.0** and absent from **2.6.1**; Tier 1 caught the three
|
|
135
|
+
*subpaths* that moved with them, and the twelve *names* were found only by a human reading package
|
|
136
|
+
internals.
|
|
137
|
+
|
|
138
|
+
**Deliberately permissive, and here is the price.** A name passes when it appears as a *word
|
|
139
|
+
anywhere* in the package's `.d.ts` closure — not only in an export position, not only behind the
|
|
140
|
+
subpath it was imported from — and the closure follows `export … from '<other-package>'` re-exports
|
|
141
|
+
one level out to another installed package. Intra-package barrels (`export * from './X'`) need no
|
|
142
|
+
following, because the whole tree is read either way. That admits names the package merely
|
|
143
|
+
*references* (an imported PrimeReact symbol, a name in a doc comment) and it will not notice a name
|
|
144
|
+
imported from the wrong subpath of the right package. The trade is deliberate: a false warning
|
|
145
|
+
trains people to ignore the guard, a missed one costs a stale line. Measured over the corpus's 85
|
|
146
|
+
import statements / 134 bindings / 38 distinct *(package, name)* pairs plus a 36-pair all-valid
|
|
147
|
+
probe: **zero false positives**, and it still flags all twelve of the 3.0.0 names above when they are
|
|
148
|
+
written unqualified.
|
|
149
|
+
|
|
150
|
+
**Same warn-only, same silence, same version rule as Tier 1.** No `jq`, no `node_modules`, a package
|
|
151
|
+
this repository does not depend on, or a package that ships no `.d.ts`: skipped without a word. A
|
|
152
|
+
name is cleared when any line in the same file that mentions it also carries a version — judged per
|
|
153
|
+
*(file, name)*, for the same reason Tier 1 judges per *(file, reference)*.
|
|
154
|
+
|
|
155
|
+
**What it deliberately does not check.** Identifiers that never appear inside an `import { … }`:
|
|
156
|
+
prose mentions, JSX usages, and C# type positions are all invisible. It reads TypeScript import
|
|
157
|
+
statements, nothing else.
|
|
158
|
+
|
|
159
|
+
Run it standalone over any roots, and add `CRATIS_HOOKS_IMPORT_REPORT=1` to see every binding and how
|
|
160
|
+
it resolved rather than only the failures.
|
|
161
|
+
|
|
162
|
+
### .NET type existence — `scripts/validate-type-references.sh` (warn)
|
|
163
|
+
|
|
164
|
+
Tier 3, and the only tier that reads .NET rather than TypeScript. Tiers 1 and 2 both start from an
|
|
165
|
+
`import` statement, so a type the corpus names *only* in prose and in C# type positions is invisible
|
|
166
|
+
to both. That is exactly how `ReactorSideEffect` survived: never a module specifier, never an import,
|
|
167
|
+
told readers to return it from a reactor, shown with object-initializer syntax — and never a type in
|
|
168
|
+
any Chronicle release. Someone following the corpus wrote code that does not compile.
|
|
169
|
+
|
|
170
|
+
**The index.** Every `Cratis*` version pinned in `Directory.Packages.props` — or, in the corpus
|
|
171
|
+
repository itself, in the tracked pin list `scripts/cratis-nuget-pins.txt`, which names the exact
|
|
172
|
+
product versions the skills verify against — plus the Cratis packages those pull in (`Cratis` is a
|
|
173
|
+
metapackage), resolved against the local NuGet cache. A pin moves only together with the skill
|
|
174
|
+
whose verified version moved.
|
|
175
|
+
|
|
176
|
+
**Exit codes.** `0` ran (warnings, if any, are on stderr); `1` a `--self-test` expectation failed;
|
|
177
|
+
`2` could not run — no pin source, no NuGet cache, or an index that came up empty — with the reason
|
|
178
|
+
on stderr. "Ran and found nothing" and "never looked" are different verdicts
|
|
179
|
+
(`exit-codes-and-wrappers.md`), and this guard spent its first lifetime erasing that difference by
|
|
180
|
+
exiting `0` at the `Directory.Packages.props` gate in a repository that has none (#287).
|
|
181
|
+
|
|
182
|
+
**Self-test.** `--self-test` seeds the motivating fabrication (`ReactorSideEffect`, in prose, in
|
|
183
|
+
attribute position, beside the real names it must be distinguished from) into a scratch corpus and
|
|
184
|
+
fails unless the guard names it and keeps the real types silent. Run it after any change to the
|
|
185
|
+
extraction rules, the pin list, or the allowlist — a guard that can pass vacuously is worse than no
|
|
186
|
+
guard (`guards-and-fuses.md`). Each package's
|
|
187
|
+
`lib/**/*.xml` carries `<member name="T:Full.Namespace.TypeName">` — a complete machine-readable type
|
|
188
|
+
list — and every other identifier the docs mention is kept as a second, permissive accept list, in
|
|
189
|
+
the same spirit as Tier 2's "a word anywhere in the `.d.ts` closure". Names the corpus itself
|
|
190
|
+
declares, and names declared in the consuming repository's own `Source/**/*.cs`, are accepted too: a worked
|
|
191
|
+
example that writes `public record AuthorRegistered(…)` before using it is not documenting a
|
|
192
|
+
framework API. A curated allowlist covers the rest — see below.
|
|
193
|
+
|
|
194
|
+
**Why it is narrow, and what that cost.** The naive version of this check is the reason the whole
|
|
195
|
+
tier nearly did not ship. Of the **1279** distinct PascalCase names it reads across 151 corpus files,
|
|
196
|
+
**599 — 47% — resolve nowhere**, because the corpus legitimately invents domain examples
|
|
197
|
+
(`AuthorRegistered`, `IAuthorService`), placeholders and prose nouns. A guard that cries wolf 599
|
|
198
|
+
times gets switched off, and then it protects nothing. So only two constructs are ever reported:
|
|
199
|
+
|
|
200
|
+
| Construct | Why it is safe | Measured |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| **Attribute position** — `[Name]`, `[Name<T>]`, `[Name(…)]` inside an inline code span or a fenced `csharp` block | attribute brackets are unambiguous C#, and a markdown link cannot live inside a code span, so the syntax alone identifies an API reference; `Name` and `NameAttribute` both count | 686 occurrences, 61 distinct names |
|
|
203
|
+
| **Framework-adjacent type token** — any other PascalCase token in a code span or a fenced `csharp` block that resolves nowhere **and** is a strict PascalCase-word-boundary *prefix* of a real Cratis type name | that is the fabrication signature: a half-remembered real family of names with a member coined that was never minted. `ReactorSideEffect` is a prefix of `ReactorSideEffectFailure`; `AuthorRegistered` is a prefix of nothing Cratis ships | takes the 599 unresolved down to **2** |
|
|
204
|
+
|
|
205
|
+
Both remaining names — `ICommand` and `IQuery`, which do not exist — are cleared by the absence rule
|
|
206
|
+
below, because the corpus's own point about them is exactly that. **Zero warnings on the real
|
|
207
|
+
corpus.**
|
|
208
|
+
|
|
209
|
+
**Constructs measured and rejected.** Each was extracted over the whole corpus and its unresolved
|
|
210
|
+
names counted before being dropped: `new TypeName` in a fenced `csharp` block (**17** false positives —
|
|
211
|
+
example events are constructed but never declared), `IInterfaceName` in a fenced `csharp` block (**17** —
|
|
212
|
+
invented example services like `IOrderRepository`), the same in an inline code span (**23** —
|
|
213
|
+
TypeScript interfaces and shouty prose such as `IMPORTANT`), and in bare prose (**2**, including the
|
|
214
|
+
plural `IDs`). None of them survives the "precision over recall" test on its own. They are all still
|
|
215
|
+
*read*; they simply have to earn a warning through framework-adjacency instead of through syntax.
|
|
216
|
+
|
|
217
|
+
**Three structural exclusions, no allowlist needed.** A token is skipped when it is preceded by `.`
|
|
218
|
+
(a member, not a type), when it is ALL-CAPS (`PII`, `IMPORTANT`), and when it is written as
|
|
219
|
+
`<Placeholder>` — the corpus's `<Module>/<Feature>/<Slice>` idiom, distinguished from a generic
|
|
220
|
+
argument list by the character before the `<`, which in C# is always an identifier character.
|
|
221
|
+
|
|
222
|
+
**Same warn-only and same version rule as Tiers 1 and 2, plus one of its own.** A name is cleared
|
|
223
|
+
when any line in the same file that mentions it carries a version, *or* says the thing does not
|
|
224
|
+
exist — `does not exist`, `no longer`, `never use`, `removed`, `deprecated`, `there is no` and
|
|
225
|
+
friends. Part of this corpus's job is naming APIs that are **not** real, and warning about a line
|
|
226
|
+
whose entire point is that the type is fictional would be the most annoying false positive of all.
|
|
227
|
+
The cost is stated plainly: reintroduce a fabrication into a sentence containing one of those
|
|
228
|
+
phrases and the guard stays quiet.
|
|
229
|
+
|
|
230
|
+
**Silent when it cannot judge.** No `Directory.Packages.props`, no local NuGet cache, or a cache
|
|
231
|
+
holding none of the pinned versions: skipped without a word. It needs no `jq` and no `node_modules`,
|
|
232
|
+
which is why Tier 1 invokes it *above* its own gates rather than beside the Tier 2 call — a backend-
|
|
233
|
+
only repository must still get this check. It adds about 1.4 s to `validate-ai-setup.sh`.
|
|
234
|
+
|
|
235
|
+
**The allowlist — `scripts/type-references-allowlist.txt`.** Thirteen entries, each with a written
|
|
236
|
+
justification: ASP.NET Core and BCL attributes that live in ref packs (which ship no XML docs at
|
|
237
|
+
all), Orleans and `Microsoft.Extensions.*` attributes from packages that ship none either, `[CliCommand]`
|
|
238
|
+
/ `[CliExample]` from the separate `Cratis/cli` repository, the Chronicle **Kernel**'s `WellKnown`,
|
|
239
|
+
and `@cratis/fundamentals`' TypeScript `JsonSerializer`. Every one was verified real before being
|
|
240
|
+
listed. An entry is a small lie the guard tells itself, so prefer widening the index whenever that
|
|
241
|
+
is possible, and never add a name you have not confirmed exists.
|
|
242
|
+
|
|
243
|
+
**What it deliberately does not check.** TypeScript — that is Tiers 1 and 2. Members, methods and
|
|
244
|
+
properties: `Provide()`, `.AutoMap()` and `EventStoreName.NotSet` are all invisible, and a fabricated
|
|
245
|
+
*member* on a real type would pass. And a fabricated type that is not a prefix of any real Cratis
|
|
246
|
+
name is invisible too — the adjacency filter is what buys the precision, and it is also the ceiling
|
|
247
|
+
on the recall.
|
|
248
|
+
|
|
249
|
+
Run it standalone over any roots, and add `CRATIS_HOOKS_TYPE_REPORT=1` to see every distinct name and
|
|
250
|
+
how it resolved rather than only the failures.
|
|
251
|
+
|
|
252
|
+
## Configuration is data, not code
|
|
253
|
+
|
|
254
|
+
Neither the pattern list nor the gate commands live in a script. A consuming repository
|
|
255
|
+
customises both without forking anything:
|
|
256
|
+
|
|
257
|
+
| File | Purpose |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `scripts/cratis-patterns.json` | shipped pattern set; its header `$comment` documents every field |
|
|
260
|
+
| `scripts/cratis-patterns.local.json` | optional; merged over the above by `id` — add patterns, or set `"enabled": false` to silence one |
|
|
261
|
+
| `scripts/quality-gates.json` | shipped gates; `changed` globs decide when a gate runs, `requires` and `workingDirectoryFrom` decide whether it *can* |
|
|
262
|
+
|
|
263
|
+
A gate whose `requires.commands` are not on `PATH`, whose `requires.paths` do not exist, or whose
|
|
264
|
+
`workingDirectoryFrom` matches nothing in the repository, is a **no-op with a message on stderr**
|
|
265
|
+
rather than a failure — that is how a repository with no .NET solution or no frontend stays quiet.
|
|
266
|
+
|
|
267
|
+
**No shipped gate names a product's file.** A default that did would activate in exactly one
|
|
268
|
+
repository and silently no-op in every other, which is the worst of both: it looks configured and
|
|
269
|
+
checks nothing. So the .NET and frontend gates state *what kind of project* they build and let the
|
|
270
|
+
gate script find it — `workingDirectoryFrom: ["*.slnx", "*.sln", "**/*.slnx", "**/*.sln"]` runs
|
|
271
|
+
`dotnet build` in whichever directory holds the repository's own solution, preferring one at the
|
|
272
|
+
root because the globs are tried in order. The frontend gates discover `package.json` the same way.
|
|
273
|
+
The same shipped file therefore activates in an application repository, activates in a framework
|
|
274
|
+
repository, and stays quiet in a corpus-only repository like this one, which has no project at all.
|
|
275
|
+
|
|
276
|
+
**Overriding it, in order of increasing force.** Set `workingDirectory` on a gate to pin one of
|
|
277
|
+
several candidate projects; drop a `quality-gates.json` of your own in place of the shipped one; or
|
|
278
|
+
point `CRATIS_HOOKS_GATES` at a file anywhere. None of them requires forking the script.
|
|
279
|
+
|
|
280
|
+
**Profile note.** The C# patterns are application-profile and scoped to `Source/**/*.cs`, which is
|
|
281
|
+
the application source root [`../rules/general.md`](../rules/general.md) documents — not a path in
|
|
282
|
+
this repository, which has no C# at all. A framework-profile repository (Arc, Chronicle,
|
|
283
|
+
Fundamentals, Components — see [`../rules/framework.md`](../rules/framework.md)) has no vertical
|
|
284
|
+
slices and should disable them in its `cratis-patterns.local.json`; a repository whose application
|
|
285
|
+
source root is not `Source/` re-scopes the `paths` globs there too.
|
|
286
|
+
|
|
287
|
+
**One property gates the proxy generator.** The generator's MSBuild target is
|
|
288
|
+
`Condition="'$(CratisProxiesOutputPath)' != ''"`, so clearing that property with
|
|
289
|
+
`-p:CratisProxiesOutputPath=` is the *only* way to make it no-op. There is no
|
|
290
|
+
`DisableProxyGenerator` property — MSBuild silently accepts unknown `-p:` names, so passing one
|
|
291
|
+
looks like it works and changes nothing. A consuming repository's build workflow should split the
|
|
292
|
+
two configurations the way the shipped gates do: Release clears the path, Debug does not, because
|
|
293
|
+
`general.md` makes the Debug build the canonical trigger for regenerating the TypeScript proxies
|
|
294
|
+
the frontend phase depends on.
|
|
295
|
+
|
|
296
|
+
## Escape hatches
|
|
297
|
+
|
|
298
|
+
Each is an explicit, auditable opt-out — none of them is a default.
|
|
299
|
+
|
|
300
|
+
| Variable | Effect |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `CRATIS_HOOKS_ALLOW_PROTECTED_WRITES=1` | allows one protected write; this is the "unless explicitly asked" case for dependency manifests |
|
|
303
|
+
| `CRATIS_HOOKS_SKIP_SCAN=1` | disables the pattern pass |
|
|
304
|
+
| `CRATIS_HOOKS_SKIP_GATE=1` | disables the quality gate |
|
|
305
|
+
| `CRATIS_HOOKS_GATE_DRYRUN=1` | prints which gates would run, and why, then exits 0 |
|
|
306
|
+
| `CRATIS_HOOKS_PATTERNS=<path>` | replaces the pattern file |
|
|
307
|
+
| `CRATIS_HOOKS_GATES=<path>` | replaces the gate file |
|
|
308
|
+
| `CRATIS_HOOKS_SUBPATH_REPORT=1` | prints every `@cratis/*` subpath reference and how it resolved, not only the failures |
|
|
309
|
+
| `CRATIS_HOOKS_IMPORT_REPORT=1` | prints every `@cratis/*` named import binding and how it resolved, not only the failures |
|
|
310
|
+
| `CRATIS_HOOKS_TYPE_REPORT=1` | prints every .NET type/attribute name the corpus mentions and how it resolved, not only the failures |
|
|
311
|
+
|
|
312
|
+
## Design constraints
|
|
313
|
+
|
|
314
|
+
- **POSIX-safe bash**, `set -euo pipefail`, quoted expansions, no `eval`. Verified on bash 3.2
|
|
315
|
+
(macOS system bash) — no `mapfile`, no associative arrays, no GNU-only flags, `LC_ALL=C` on
|
|
316
|
+
every sort and compare.
|
|
317
|
+
- **Gate commands are an argv array**, executed directly. They never pass through a shell.
|
|
318
|
+
- **`jq` is the only dependency.** Every script
|
|
319
|
+
degrades to a silent no-op when it is missing — a hook must never break a session.
|
|
320
|
+
- **Fail safe.** Malformed config, empty stdin, a missing file, a binary file, a file over 2 MB:
|
|
321
|
+
all exit 0 silently.
|
|
322
|
+
- **No secrets, no file dumps.** Gate output is capped at `maxOutputLines`; the pattern pass
|
|
323
|
+
prints a path, a line number and a fixed message — never file content.
|
|
324
|
+
- **No re-entry.** The `Stop` hook returns immediately when `stop_hook_active` is true, so a
|
|
325
|
+
blocked turn cannot loop.
|
|
326
|
+
- **Each pattern fires once per file per session**, tracked under
|
|
327
|
+
`${TMPDIR}/cratis-hooks/<session-id>/`, so a long edit loop cannot flood context.
|
|
328
|
+
- **The gate never edits code.** It builds, tests and lints. The one side effect is that a Debug
|
|
329
|
+
build regenerates TypeScript proxies, which is the documented purpose of that build.
|
|
330
|
+
|
|
331
|
+
## Verifying a change
|
|
332
|
+
|
|
333
|
+
The scripts read hook JSON on stdin, so they are directly testable:
|
|
334
|
+
|
|
335
|
+
The pattern pass and the gate both read the repository they are pointed at, so testing them means
|
|
336
|
+
pointing them at a repository that *has* the thing under test. This corpus has no C# and no
|
|
337
|
+
project, so run those two against a consuming checkout (or a scratch tree), and expect silence here.
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
# Pattern pass — expect exit 0, and JSON on stdout only when something matched.
|
|
341
|
+
# Run from an application checkout; <Module>/<Feature>/<Slice> is the layout general.md documents.
|
|
342
|
+
jq -nc '{session_id:"t", cwd:"'"$PWD"'", tool_name:"Edit",
|
|
343
|
+
tool_input:{file_path:"'"$PWD"'/Source/<Module>/<Feature>/<Slice>/<Slice>.cs"}}' \
|
|
344
|
+
| .cratis/ai/hooks/scripts/cratis-pattern-scan.sh; echo "exit=$?"
|
|
345
|
+
|
|
346
|
+
# Hard block — expect exit 2
|
|
347
|
+
jq -nc '{session_id:"t", cwd:"'"$PWD"'", tool_name:"Edit",
|
|
348
|
+
tool_input:{file_path:"'"$PWD"'/Directory.Packages.props", new_string:"x"}}' \
|
|
349
|
+
| .cratis/ai/hooks/scripts/cratis-guard-writes.sh; echo "exit=$?"
|
|
350
|
+
|
|
351
|
+
# Quality gate — show the dispatch plan without running anything
|
|
352
|
+
jq -nc '{session_id:"t", cwd:"'"$PWD"'", stop_hook_active:false}' \
|
|
353
|
+
| CRATIS_HOOKS_GATE_DRYRUN=1 .cratis/ai/hooks/scripts/cratis-quality-gate.sh
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
The subpath guard takes corpus roots as arguments, so it is testable in both directions without
|
|
357
|
+
touching the corpus — point it at a scratch folder holding a known-bad reference, then at the real
|
|
358
|
+
roots. A one-sided test passes vacuously; run both.
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
# Negative — expect a warning naming the file and line
|
|
362
|
+
mkdir -p /tmp/scratch-corpus
|
|
363
|
+
echo "import x from '@cratis/components/ThisDoesNotExist';" > /tmp/scratch-corpus/drift.md
|
|
364
|
+
.cratis/ai/hooks/scripts/validate-package-subpaths.sh .cratis/ai/rules /tmp/scratch-corpus
|
|
365
|
+
|
|
366
|
+
# Positive — expect silence, and the report to show every real reference resolving
|
|
367
|
+
CRATIS_HOOKS_SUBPATH_REPORT=1 .cratis/ai/hooks/scripts/validate-package-subpaths.sh
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Tier 2 is testable the same way, and wants a third run the subpath guard does not: a probe of names
|
|
371
|
+
that all genuinely exist. A guard that warns on everything passes the negative test just as well as
|
|
372
|
+
a correct one, so prove it stays quiet when it should.
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# Negative — a fabricated name behind a subpath that resolves
|
|
376
|
+
mkdir -p /tmp/scratch-corpus
|
|
377
|
+
echo "import { CommandDialog, ThisNameDoesNotExist } from '@cratis/components/CommandDialog';" \
|
|
378
|
+
> /tmp/scratch-corpus/drift.md
|
|
379
|
+
.cratis/ai/hooks/scripts/validate-package-imports.sh /tmp/scratch-corpus
|
|
380
|
+
|
|
381
|
+
# Discrimination — every name real, expect silence
|
|
382
|
+
echo "import { DataPage, MenuItem } from '@cratis/components/DataPage';" \
|
|
383
|
+
> /tmp/scratch-corpus/drift.md
|
|
384
|
+
.cratis/ai/hooks/scripts/validate-package-imports.sh /tmp/scratch-corpus
|
|
385
|
+
|
|
386
|
+
# Positive — the real corpus, with the report showing every binding resolving
|
|
387
|
+
CRATIS_HOOKS_IMPORT_REPORT=1 .cratis/ai/hooks/scripts/validate-package-imports.sh
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Tier 3 wants the same three runs, and its negative case is the one that motivated it. Put
|
|
391
|
+
`ReactorSideEffect` back into a scratch corpus and the guard must name it; a design that misses its
|
|
392
|
+
own motivating case is the wrong design.
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
# Negative — the confirmed fabrication, in prose and in object-initializer syntax
|
|
396
|
+
mkdir -p /tmp/scratch-corpus
|
|
397
|
+
printf 'A reactor may return a `ReactorSideEffect` to control where the event is appended.\n' \
|
|
398
|
+
> /tmp/scratch-corpus/drift.md
|
|
399
|
+
.cratis/ai/hooks/scripts/validate-type-references.sh /tmp/scratch-corpus
|
|
400
|
+
|
|
401
|
+
# Discrimination — every name real, expect silence
|
|
402
|
+
printf 'Return `EventForEventSourceId`, or a `ReactorSideEffectFailure` from an `IReactor`.\n' \
|
|
403
|
+
> /tmp/scratch-corpus/drift.md
|
|
404
|
+
.cratis/ai/hooks/scripts/validate-type-references.sh /tmp/scratch-corpus
|
|
405
|
+
|
|
406
|
+
# Positive — the real corpus, expect silence, with the report showing how each name resolved
|
|
407
|
+
CRATIS_HOOKS_TYPE_REPORT=1 .cratis/ai/hooks/scripts/validate-type-references.sh
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Run `bash -n` on every script and `jq .` on every JSON file before committing. The hook scripts are
|
|
411
|
+
kept at **zero** `shellcheck --external-sources --severity=style` findings by the **Lint the hook
|
|
412
|
+
scripts** step of the `Verify AI Corpus` workflow (`.github/workflows/verify-ai-corpus.yml`), which
|
|
413
|
+
fails the run on any finding at that severity or above. Run the same command before committing:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
shellcheck --external-sources --severity=style .cratis/ai/hooks/scripts/*.sh
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The CI step counts the scripts it checked and refuses to pass on an empty population, so a glob that
|
|
420
|
+
stops matching is a failure rather than a silent green. A finding that is genuinely a false positive
|
|
421
|
+
is silenced with a `# shellcheck disable=SC…` directive carrying a comment that says why — never by
|
|
422
|
+
loosening the severity.
|
|
423
|
+
|
|
424
|
+
The step uses whatever shellcheck the runner image ships, and prints its version first. Different
|
|
425
|
+
versions genuinely disagree: 0.9.0 flags `A && B || C` (SC2015) where 0.11.0 does not, so a local
|
|
426
|
+
run can be green while CI is red. The scripts are currently clean under **both** 0.9.0 and 0.11.0.
|
|
427
|
+
If a runner image upgrade introduces a new finding, fix the script — the version line at the top of
|
|
428
|
+
the step log says which version changed its mind.
|
|
429
|
+
|
|
430
|
+
## Note on `.claude/settings.local.json`
|
|
431
|
+
|
|
432
|
+
If that file carries `allow` entries for `Bash(git push *)` and `Bash(gh pr *)`, they win: local
|
|
433
|
+
settings take precedence over project settings, so they override the `ask` entries the template
|
|
434
|
+
puts in `.claude/settings.json`. Remove them there if you want the confirmation prompt back.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
lifecycle: session-stop
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Agent Stop — Build, Specs, and Corpus Validation
|
|
6
|
+
|
|
7
|
+
> **This is lifecycle guidance, not a wired tool hook.** Markdown is not a hook format for Copilot or Claude Code. To *enforce* it, wire it per tool to run the repo's build/test command — Claude Code: a `Stop` hook in `.claude/settings.json`; GitHub Copilot: a `sessionEnd` entry in a `.github/hooks/*.json` file. The steps below are what that hook (or the agent) should do.
|
|
8
|
+
|
|
9
|
+
When the agent finishes a session, verify the work against **fresh signals** before stopping — never against self-assessment. Pick the path that matches the repository.
|
|
10
|
+
|
|
11
|
+
## Pick the path for this repository
|
|
12
|
+
|
|
13
|
+
- **AI corpus repo** — the changes are only under `.cratis/ai/`, `.github/`, or `.claude/` and there is no .NET solution or frontend to build (e.g. this `cratis/AI` repo). Run the AI-setup validator instead of a code build:
|
|
14
|
+
```
|
|
15
|
+
.cratis/ai/hooks/scripts/validate-ai-setup.sh
|
|
16
|
+
```
|
|
17
|
+
Stop only when it passes (symlinks/adapters healthy, frontmatter present, no broken cross-links). Skip the application gates below.
|
|
18
|
+
|
|
19
|
+
- **Application repo** — there is a .NET solution and/or a frontend. Run the application gates below.
|
|
20
|
+
|
|
21
|
+
## Application gates
|
|
22
|
+
|
|
23
|
+
1. **Clean** from repository root:
|
|
24
|
+
```
|
|
25
|
+
dotnet clean
|
|
26
|
+
```
|
|
27
|
+
2. **Build Debug** from repository root — validates `#if DEBUG` spec code and regenerates the TypeScript proxies:
|
|
28
|
+
```
|
|
29
|
+
dotnet build
|
|
30
|
+
```
|
|
31
|
+
3. **Build Release** from repository root — build-only check; skip re-running proxy generation:
|
|
32
|
+
```
|
|
33
|
+
dotnet build -c Release -p:CratisProxiesOutputPath=
|
|
34
|
+
```
|
|
35
|
+
4. **Run specs/tests for every affected project** — use the project's test command; if you cannot isolate the affected scope, run the repository-level test command.
|
|
36
|
+
5. **Frontend** (when frontend files changed) — run lint, the type/build check, and frontend tests.
|
|
37
|
+
|
|
38
|
+
## If any gate fails
|
|
39
|
+
|
|
40
|
+
- Report the full output.
|
|
41
|
+
- Fix all errors, warnings, and failing specs before considering the session complete.
|
|
42
|
+
- Re-run the gate that failed and confirm it passes *this time*.
|
|
43
|
+
|
|
44
|
+
## Rules
|
|
45
|
+
|
|
46
|
+
- A session is not complete until both Debug and Release builds exit `0` with **zero** warnings, and the affected specs/tests exit `0`.
|
|
47
|
+
- Treat Release-only warnings (nullable annotations, analyzer findings) as errors — fix them.
|
|
48
|
+
- **Never** use `/clp:ErrorsOnly` or any flag that suppresses warning output — hidden warnings are warnings that never get fixed.
|
|
49
|
+
- A green build is not behavioral correctness — exercise the affected behavior (specs, or the running UI) and state plainly anything you could not verify.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
lifecycle: pre-commit
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Pre-commit — Run Specs
|
|
6
|
+
|
|
7
|
+
> **This is lifecycle guidance, not a wired tool hook.** To *enforce* it, wire it per tool — Claude Code: a `PreToolUse` hook in `.claude/settings.json` with a matcher on `Bash` (or your terminal tool) gating `git commit` (and its rtk-rewritten `rtk git commit` form — see [rtk](../rules/rtk.md)); GitHub Copilot: a hook in a `.github/hooks/*.json` file. The steps below are what that hook (or the agent) should do.
|
|
8
|
+
|
|
9
|
+
Before an explicitly authorized commit, verify the staged scope with proportional checks. Reuse fresh passing results only when they cover the exact unchanged staged inputs; otherwise run the relevant checks. Never stage unrelated edits.
|
|
10
|
+
|
|
11
|
+
## When this guidance applies
|
|
12
|
+
|
|
13
|
+
Apply before an authorized `git commit`, including `rtk git commit` or `rtk proxy git commit`. Do not interpret recognizing a command as authorization. History rewriting (`commit --amend`, rebase, squash, or force-push) remains prohibited.
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
|
|
17
|
+
1. **Confirm authorization and scope** — this guidance does not authorize a commit or create executable hook wiring. Select documentation/corpus checks for rule-only edits; do not run application tests without affected application code.
|
|
18
|
+
|
|
19
|
+
2. **Identify affected projects** from the staged changes:
|
|
20
|
+
```
|
|
21
|
+
git diff --name-only --cached
|
|
22
|
+
```
|
|
23
|
+
Collect unique affected project roots:
|
|
24
|
+
- `.cs` files → walk up to the nearest `.csproj`.
|
|
25
|
+
- `.ts` / `.tsx` files → walk up to the nearest `package.json` with a `"test"` script.
|
|
26
|
+
|
|
27
|
+
3. **Run specs for each affected .NET project**:
|
|
28
|
+
```
|
|
29
|
+
dotnet test <specs-project-path> --no-build
|
|
30
|
+
```
|
|
31
|
+
Use `--no-build` only when matching build outputs are current; otherwise incrementally build the affected specs project first. If the owning specs project cannot be identified, inspect project references or report the uncertainty; do not default to a root-wide test run.
|
|
32
|
+
|
|
33
|
+
4. **Run specs for each affected TypeScript project**:
|
|
34
|
+
```
|
|
35
|
+
yarn test
|
|
36
|
+
```
|
|
37
|
+
Run from the package root that owns the changed files.
|
|
38
|
+
|
|
39
|
+
5. **If a relevant check fails** — diagnose within a bounded attempt, fix only in-scope causes, and re-run the failed gate. Report unrelated/environmental failures as blockers instead of repeated retries or broad edits. Do not claim completion or bypass required gates.
|
|
40
|
+
|
|
41
|
+
6. **When relevant required checks pass** — proceed only with the originally authorized commit and staged scope. Report the exact verification and any checks not run.
|
|
42
|
+
|
|
43
|
+
## Rules
|
|
44
|
+
|
|
45
|
+
- Documentation/rule-only commits run relevant content, link, frontmatter, and corpus checks, not application builds/tests.
|
|
46
|
+
- Code changes run affected-project incremental checks and targeted regression specs after coherent changes. Wider suites and clean/Release builds require cross-cutting scope or repository merge/release gates.
|
|
47
|
+
- Do not bypass required failures, suppress diagnostics, or expand into unrelated cleanup. Missing prerequisites and pre-existing failures must be reported honestly.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook — hard block on writes that must never happen.
|
|
3
|
+
#
|
|
4
|
+
# Exits 2 (block the tool call, stderr goes back to the model) for:
|
|
5
|
+
# 1. Generated files — anything whose header marks it as Cratis-generated output
|
|
6
|
+
# (.cratis/ai/rules/general.md rule 15 [contract])
|
|
7
|
+
# 2. Dependency manifests — Directory.Packages.props, global.json, lockfiles, NuGet config
|
|
8
|
+
# 3. Environment files — .env and friends (secrets)
|
|
9
|
+
#
|
|
10
|
+
# 2 and 3 come from the Source-of-Truth Discipline rule: "Don't change dependency manifests /
|
|
11
|
+
# lockfiles / global.json / NuGet config unless explicitly asked."
|
|
12
|
+
#
|
|
13
|
+
# Escape hatch for the "unless explicitly asked" case — the user asks, you set it for the call:
|
|
14
|
+
# CRATIS_HOOKS_ALLOW_PROTECTED_WRITES=1
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
# SCRIPTDIR, not a path relative to the caller: shellcheck resolves a plain relative `source=`
|
|
18
|
+
# against the current working directory, and these hooks are linted from wherever CI happens to run.
|
|
19
|
+
# shellcheck source=SCRIPTDIR/hook-lib.sh
|
|
20
|
+
. "$(dirname "${BASH_SOURCE[0]}")/hook-lib.sh"
|
|
21
|
+
|
|
22
|
+
[ "${CRATIS_HOOKS_ALLOW_PROTECTED_WRITES:-0}" = "1" ] && exit 0
|
|
23
|
+
|
|
24
|
+
input="$(hook_read_stdin)"
|
|
25
|
+
[ -n "$input" ] || exit 0
|
|
26
|
+
hook_have jq || exit 0
|
|
27
|
+
|
|
28
|
+
root="$(hook_repo_root)"
|
|
29
|
+
cwd="$(hook_json "$input" '.cwd')"
|
|
30
|
+
[ -n "$cwd" ] || cwd="$root"
|
|
31
|
+
|
|
32
|
+
file="$(hook_json "$input" '.tool_input.file_path')"
|
|
33
|
+
[ -n "$file" ] || file="$(hook_json "$input" '.tool_input.notebook_path')"
|
|
34
|
+
[ -n "$file" ] || exit 0
|
|
35
|
+
|
|
36
|
+
file="$(hook_abspath "$file" "$cwd")"
|
|
37
|
+
rel="$(hook_relpath "$file" "$root")"
|
|
38
|
+
base="$(basename "$file")"
|
|
39
|
+
|
|
40
|
+
block() {
|
|
41
|
+
printf 'BLOCKED by cratis-guard-writes: %s\n\n%s\n\n%s\n' "$rel" "$1" "$2" >&2
|
|
42
|
+
exit 2
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# ── 1. Generated files ────────────────────────────────────────────────────────
|
|
46
|
+
# The marker must be a real header: a comment opener at the start of one of the first few lines.
|
|
47
|
+
# Merely *mentioning* the string — documentation, a rule file, this corpus — is not a match.
|
|
48
|
+
marker='^[[:space:]]*(//|/\*|#|<!--)[[:space:]]*@generated by Cratis'
|
|
49
|
+
|
|
50
|
+
if [ -f "$file" ] \
|
|
51
|
+
&& LC_ALL=C grep -Iq . "$file" 2>/dev/null \
|
|
52
|
+
&& head -n 5 "$file" 2>/dev/null | LC_ALL=C grep -Eq "$marker"; then
|
|
53
|
+
block \
|
|
54
|
+
"This file is generated output (its header marks it as generated by Cratis). Rule 15 [contract] in .cratis/ai/rules/general.md: never edit a generated file — the next build overwrites it." \
|
|
55
|
+
"Instead: change the C# source it is generated from (the [Command] / [ReadModel] record) and rebuild with 'dotnet build -c Debug' to regenerate the proxy."
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# The same guard for authoring a *new* file that pretends to be generated output.
|
|
59
|
+
new_content="$(hook_json "$input" '.tool_input.content')"
|
|
60
|
+
[ -n "$new_content" ] || new_content="$(hook_json "$input" '.tool_input.new_string')"
|
|
61
|
+
if [ -n "$new_content" ] \
|
|
62
|
+
&& printf '%s\n' "$new_content" | head -n 5 | LC_ALL=C grep -Eq "$marker"; then
|
|
63
|
+
block \
|
|
64
|
+
"The content being written opens with a Cratis generated-file header. Proxies are produced by the Cratis proxy generator, never hand-written." \
|
|
65
|
+
"Instead: write the C# source and run 'dotnet build -c Debug' to generate the proxy."
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ── 2. Dependency manifests, lockfiles, SDK and NuGet configuration ───────────
|
|
69
|
+
case "$base" in
|
|
70
|
+
Directory.Packages.props | global.json | NuGet.config | NuGet.Config | nuget.config \
|
|
71
|
+
| yarn.lock | package-lock.json | pnpm-lock.yaml | packages.lock.json)
|
|
72
|
+
block \
|
|
73
|
+
"'$base' is a dependency manifest / lockfile / SDK or NuGet configuration file. .cratis/ai/rules/general.md (Source-of-Truth Discipline): don't change these unless explicitly asked." \
|
|
74
|
+
"If the user explicitly asked for this change, re-run the tool call with CRATIS_HOOKS_ALLOW_PROTECTED_WRITES=1 exported. Otherwise take the dependency as given and solve the problem in source."
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
|
|
78
|
+
# ── 3. Environment files ──────────────────────────────────────────────────────
|
|
79
|
+
case "$base" in
|
|
80
|
+
.env | .env.* | *.env)
|
|
81
|
+
block \
|
|
82
|
+
"'$base' is an environment file and normally holds secrets. Hooks block writing it so credentials are never authored, rewritten, or echoed by an agent." \
|
|
83
|
+
"Instead: tell the user exactly which variable to set and let them edit the file. If they explicitly asked you to write it, re-run with CRATIS_HOOKS_ALLOW_PROTECTED_WRITES=1 exported."
|
|
84
|
+
;;
|
|
85
|
+
esac
|
|
86
|
+
|
|
87
|
+
exit 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Cratis NuGet packages and exact versions the canonical corpus verifies against.
|
|
2
|
+
# One `Id Version` pair per line; the type-reference guard's transitive walk expands
|
|
3
|
+
# each into the full Cratis closure found in the local NuGet cache. Sources: the
|
|
4
|
+
# "Verified product sources" sections of the skills under skills/.
|
|
5
|
+
# Update a pin only together with the skill whose verified version moved.
|
|
6
|
+
Cratis.Arc 22.10.4
|
|
7
|
+
Cratis.Arc.Chronicle 22.10.4
|
|
8
|
+
Cratis.Chronicle 16.45.2
|
|
9
|
+
Cratis.Chronicle 17.0.1
|
|
10
|
+
Cratis.Chronicle.CodeAnalysis 16.45.3
|
|
11
|
+
Cratis.Fundamentals 7.18.2
|