@nitra/cursor 1.9.21 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-template/hooks/capture-decisions.sh +3 -3
- package/.claude-template/hooks/normalize-decisions.sh +370 -0
- package/CHANGELOG.md +43 -0
- package/bin/n-cursor.js +56 -49
- package/package.json +10 -5
- package/rules/abie/auto.md +1 -0
- package/{scripts/check-abie.mjs → rules/abie/js/check.mjs} +5 -5
- package/rules/adr/adr.mdc +150 -0
- package/{scripts/check-adr.mjs → rules/adr/js/check.mjs} +85 -41
- package/rules/adr/policy/settings_json/settings_json.rego +37 -0
- package/rules/adr/policy/settings_local_json/settings_local_json.rego +40 -0
- package/rules/bun/auto.md +1 -0
- package/{scripts/check-bun.mjs → rules/bun/js/check.mjs} +1 -1
- package/rules/capacitor/auto.md +1 -0
- package/{scripts/check-capacitor.mjs → rules/capacitor/js/check.mjs} +1 -1
- package/rules/changelog/auto.md +1 -0
- package/{scripts/check-changelog.mjs → rules/changelog/js/check.mjs} +2 -2
- package/rules/docker/auto.md +1 -0
- package/{scripts/check-docker.mjs → rules/docker/js/check.mjs} +5 -5
- package/{scripts/run-docker.mjs → rules/docker/js/run.mjs} +5 -5
- package/rules/ga/auto.md +1 -0
- package/{scripts/check-ga.mjs → rules/ga/js/check.mjs} +4 -4
- package/{scripts/lint-ga.mjs → rules/ga/js/lint.mjs} +2 -2
- package/rules/graphql/auto.md +1 -0
- package/{scripts/check-graphql.mjs → rules/graphql/js/check.mjs} +5 -5
- package/rules/hasura/auto.md +1 -0
- package/{scripts/check-hasura.mjs → rules/hasura/js/check.mjs} +4 -4
- package/rules/image-avif/auto.md +1 -0
- package/{scripts/check-image-avif.mjs → rules/image-avif/js/check.mjs} +5 -5
- package/rules/image-compress/auto.md +1 -0
- package/{scripts/check-image-compress.mjs → rules/image-compress/js/check.mjs} +1 -1
- package/rules/js-bun-db/auto.md +1 -0
- package/{scripts/check-js-bun-db.mjs → rules/js-bun-db/js/check.mjs} +5 -5
- package/rules/js-bun-redis/auto.md +1 -0
- package/{scripts/check-js-bun-redis.mjs → rules/js-bun-redis/js/check.mjs} +4 -4
- package/rules/js-lint/auto.md +1 -0
- package/{scripts/check-js-lint.mjs → rules/js-lint/js/check.mjs} +1 -1
- package/rules/js-mssql/auto.md +1 -0
- package/{scripts/check-js-mssql.mjs → rules/js-mssql/js/check.mjs} +5 -5
- package/rules/js-run/auto.md +1 -0
- package/{scripts/check-js-run.mjs → rules/js-run/js/check.mjs} +11 -11
- package/rules/k8s/auto.md +1 -0
- package/{scripts/check-k8s.mjs → rules/k8s/js/check.mjs} +4 -4
- package/{scripts/run-k8s.mjs → rules/k8s/js/run.mjs} +4 -4
- package/rules/nginx-default-tpl/auto.md +1 -0
- package/{scripts/check-nginx-default-tpl.mjs → rules/nginx-default-tpl/js/check.mjs} +7 -7
- package/rules/npm-module/auto.md +1 -0
- package/{scripts/check-npm-module.mjs → rules/npm-module/js/check.mjs} +4 -4
- package/rules/php/auto.md +1 -0
- package/{scripts/check-php.mjs → rules/php/js/check.mjs} +1 -1
- package/{scripts/run-php.mjs → rules/php/js/run.mjs} +3 -3
- package/rules/rego/auto.md +1 -0
- package/{scripts/check-rego.mjs → rules/rego/js/check.mjs} +4 -4
- package/{scripts/lint-rego.mjs → rules/rego/js/lint.mjs} +1 -1
- package/rules/style-lint/auto.md +1 -0
- package/{scripts/check-style-lint.mjs → rules/style-lint/js/check.mjs} +1 -1
- package/rules/tauri/auto.md +1 -0
- package/{scripts/check-tauri.mjs → rules/tauri/js/check.mjs} +2 -2
- package/rules/text/auto.md +1 -0
- package/{scripts/check-text.mjs → rules/text/js/check.mjs} +2 -2
- package/{scripts/run-shellcheck-text.mjs → rules/text/js/run-shellcheck.mjs} +2 -2
- package/{scripts → rules/text/js}/run-v8r.mjs +2 -2
- package/{mdc → rules/text}/text.mdc +4 -0
- package/rules/vue/auto.md +1 -0
- package/{scripts/check-vue.mjs → rules/vue/js/check.mjs} +5 -5
- package/scripts/auto-rules.mjs +5 -5
- package/scripts/auto-skills.mjs +8 -6
- package/scripts/lint-conftest.mjs +13 -13
- package/scripts/sync-claude-config.mjs +70 -14
- package/scripts/utils/run-conftest-batch.mjs +9 -4
- package/skills/abie-clean/auto.md +1 -0
- package/skills/abie-kustomize/auto.md +1 -0
- package/skills/adr-normalize/SKILL.md +71 -0
- package/skills/adr-normalize/auto.md +1 -0
- package/skills/fix/auto.md +1 -0
- package/skills/lint/auto.md +1 -0
- package/skills/llm-patch/auto.md +1 -0
- package/skills/publish-telegram/auto.md +1 -0
- package/skills/taze/auto.md +1 -0
- package/bin/auto-rules.md +0 -59
- package/bin/auto-skills.md +0 -25
- package/mdc/adr.mdc +0 -86
- package/policy/adr/settings_json/settings_json.rego +0 -31
- package/policy/adr/settings_local_json/settings_local_json.rego +0 -28
- /package/{mdc → rules/abie}/abie.mdc +0 -0
- /package/{policy/abie → rules/abie/policy}/base_deployment_preem/base_deployment_preem.rego +0 -0
- /package/{policy/abie → rules/abie/policy}/base_deployment_preem/base_deployment_preem_test.rego +0 -0
- /package/{policy/abie → rules/abie/policy}/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +0 -0
- /package/{policy/abie → rules/abie/policy}/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +0 -0
- /package/{policy/abie → rules/abie/policy}/health_check_policy/health_check_policy.rego +0 -0
- /package/{policy/abie → rules/abie/policy}/health_check_policy/health_check_policy_test.rego +0 -0
- /package/{policy/abie → rules/abie/policy}/http_route_base/http_route_base.rego +0 -0
- /package/{policy/abie → rules/abie/policy}/http_route_base/http_route_base_test.rego +0 -0
- /package/{mdc → rules/bun}/bun.mdc +0 -0
- /package/{policy/bun → rules/bun/policy}/bunfig/bunfig.rego +0 -0
- /package/{policy/bun → rules/bun/policy}/package_json/package_json.rego +0 -0
- /package/{policy/bun → rules/bun/policy}/package_json/package_json_test.rego +0 -0
- /package/{mdc → rules/capacitor}/capacitor.mdc +0 -0
- /package/{policy/capacitor → rules/capacitor/policy}/package_json/package_json.rego +0 -0
- /package/{mdc → rules/changelog}/changelog.mdc +0 -0
- /package/{mdc → rules/ci4}/ci4.mdc +0 -0
- /package/{mdc → rules/docker}/docker.mdc +0 -0
- /package/{policy/docker → rules/docker/policy}/lint_docker_yml/lint_docker_yml.rego +0 -0
- /package/{policy/docker → rules/docker/policy}/lint_docker_yml/lint_docker_yml_test.rego +0 -0
- /package/{policy/docker → rules/docker/policy}/package_json/package_json.rego +0 -0
- /package/{policy/docker → rules/docker/policy}/package_json/package_json_test.rego +0 -0
- /package/{mdc → rules/ga}/ga.mdc +0 -0
- /package/{policy/ga → rules/ga/policy}/clean_ga_workflows/clean_ga_workflows.rego +0 -0
- /package/{policy/ga → rules/ga/policy}/clean_merged_branch/clean_merged_branch.rego +0 -0
- /package/{policy/ga → rules/ga/policy}/git_ai/git_ai.rego +0 -0
- /package/{policy/ga → rules/ga/policy}/lint_ga/lint_ga.rego +0 -0
- /package/{policy/ga → rules/ga/policy}/workflow_common/workflow_common.rego +0 -0
- /package/{mdc → rules/graphql}/graphql.mdc +0 -0
- /package/{policy/graphql → rules/graphql/policy}/vscode_extensions/vscode_extensions.rego +0 -0
- /package/{policy/graphql → rules/graphql/policy}/vscode_extensions/vscode_extensions_test.rego +0 -0
- /package/{mdc → rules/hasura}/hasura.mdc +0 -0
- /package/{policy/hasura → rules/hasura/policy}/svc_hl/svc_hl.rego +0 -0
- /package/{mdc → rules/image-avif}/image-avif.mdc +0 -0
- /package/{policy/image_avif → rules/image-avif/policy}/package_json/package_json.rego +0 -0
- /package/{policy/image_avif → rules/image-avif/policy}/package_json/package_json_test.rego +0 -0
- /package/{mdc → rules/image-compress}/image-compress.mdc +0 -0
- /package/{policy/image_compress → rules/image-compress/policy}/package_json/package_json.rego +0 -0
- /package/{mdc → rules/js-bun-db}/js-bun-db.mdc +0 -0
- /package/{policy/js_bun_db → rules/js-bun-db/policy}/package_json/package_json.rego +0 -0
- /package/{mdc → rules/js-bun-redis}/js-bun-redis.mdc +0 -0
- /package/{policy/js_bun_redis → rules/js-bun-redis/policy}/package_json/package_json.rego +0 -0
- /package/{mdc → rules/js-lint}/js-lint.mdc +0 -0
- /package/{policy/js_lint → rules/js-lint/policy}/lint_js_yml/lint_js_yml.rego +0 -0
- /package/{policy/js_lint → rules/js-lint/policy}/package_json/package_json.rego +0 -0
- /package/{policy/js_lint → rules/js-lint/policy}/package_json/package_json_test.rego +0 -0
- /package/{mdc → rules/js-mssql}/js-mssql.mdc +0 -0
- /package/{policy/js_mssql → rules/js-mssql/policy}/package_json/package_json.rego +0 -0
- /package/{mdc → rules/js-run}/js-run.mdc +0 -0
- /package/{policy/js_run → rules/js-run/policy}/configmap/configmap.rego +0 -0
- /package/{policy/js_run → rules/js-run/policy}/jsconfig/jsconfig.rego +0 -0
- /package/{policy/js_run → rules/js-run/policy}/jsconfig/jsconfig_test.rego +0 -0
- /package/{policy/js_run → rules/js-run/policy}/package_json/package_json.rego +0 -0
- /package/{mdc → rules/k8s}/k8s.mdc +0 -0
- /package/{policy/k8s → rules/k8s/policy}/base_kustomization/base_kustomization.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/base_kustomization/base_kustomization_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/base_manifest/base_manifest.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/base_manifest/base_manifest_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/gateway/gateway.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/gateway/gateway_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/hasura_configmap/hasura_configmap.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/hasura_configmap/hasura_configmap_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/hasura_httproute/hasura_httproute.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/hasura_httproute/hasura_httproute_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/hpa_pdb/hpa_pdb.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/hpa_pdb/hpa_pdb_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/kustomization/kustomization.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/kustomization/kustomization_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/manifest/manifest.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/manifest/manifest_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/svc_hl_yaml/svc_hl_yaml.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/svc_hl_yaml/svc_hl_yaml_test.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/svc_yaml/svc_yaml.rego +0 -0
- /package/{policy/k8s → rules/k8s/policy}/svc_yaml/svc_yaml_test.rego +0 -0
- /package/{mdc → rules/nginx-default-tpl}/nginx-default-tpl.mdc +0 -0
- /package/{policy/nginx_default_tpl → rules/nginx-default-tpl/policy}/vscode_extensions/vscode_extensions.rego +0 -0
- /package/{policy/nginx_default_tpl → rules/nginx-default-tpl/policy}/vscode_extensions/vscode_extensions_test.rego +0 -0
- /package/{policy/nginx_default_tpl → rules/nginx-default-tpl/policy}/vscode_settings/vscode_settings.rego +0 -0
- /package/{policy/nginx_default_tpl → rules/nginx-default-tpl/policy}/vscode_settings/vscode_settings_test.rego +0 -0
- /package/{mdc → rules/npm-module}/npm-module.mdc +0 -0
- /package/{policy/npm_module → rules/npm-module/policy}/emit_types_config/emit_types_config.rego +0 -0
- /package/{policy/npm_module → rules/npm-module/policy}/npm_package_json/npm_package_json.rego +0 -0
- /package/{policy/npm_module → rules/npm-module/policy}/npm_package_json/npm_package_json_test.rego +0 -0
- /package/{policy/npm_module → rules/npm-module/policy}/npm_publish_yml/npm_publish_yml.rego +0 -0
- /package/{policy/npm_module → rules/npm-module/policy}/root_package_json/root_package_json.rego +0 -0
- /package/{mdc → rules/php}/php.mdc +0 -0
- /package/{policy/php → rules/php/policy}/lint_php_yml/lint_php_yml.rego +0 -0
- /package/{policy/php → rules/php/policy}/package_json/package_json.rego +0 -0
- /package/{policy/rego → rules/rego/policy}/package_json/package_json.rego +0 -0
- /package/{policy/rego → rules/rego/policy}/package_json/package_json_test.rego +0 -0
- /package/{policy/rego → rules/rego/policy}/vscode_extensions/vscode_extensions.rego +0 -0
- /package/{policy/rego → rules/rego/policy}/vscode_extensions/vscode_extensions_test.rego +0 -0
- /package/{policy/rego → rules/rego/policy}/vscode_settings/vscode_settings.rego +0 -0
- /package/{policy/rego → rules/rego/policy}/vscode_settings/vscode_settings_test.rego +0 -0
- /package/{mdc → rules/rego}/rego.mdc +0 -0
- /package/{policy/style_lint → rules/style-lint/policy}/lint_style_yml/lint_style_yml.rego +0 -0
- /package/{policy/style_lint → rules/style-lint/policy}/package_json/package_json.rego +0 -0
- /package/{policy/style_lint → rules/style-lint/policy}/vscode_extensions/vscode_extensions.rego +0 -0
- /package/{policy/style_lint → rules/style-lint/policy}/vscode_extensions/vscode_extensions_test.rego +0 -0
- /package/{policy/style_lint → rules/style-lint/policy}/vscode_settings/vscode_settings.rego +0 -0
- /package/{policy/style_lint → rules/style-lint/policy}/vscode_settings/vscode_settings_test.rego +0 -0
- /package/{mdc → rules/style-lint}/style-lint.mdc +0 -0
- /package/{policy/tauri → rules/tauri/policy}/vscode_extensions/vscode_extensions.rego +0 -0
- /package/{policy/tauri → rules/tauri/policy}/vscode_extensions/vscode_extensions_test.rego +0 -0
- /package/{mdc → rules/tauri}/tauri.mdc +0 -0
- /package/{policy/text → rules/text/policy}/cspell/cspell.rego +0 -0
- /package/{policy/text → rules/text/policy}/markdownlint/markdownlint.rego +0 -0
- /package/{policy/text → rules/text/policy}/markdownlint/markdownlint_test.rego +0 -0
- /package/{policy/text → rules/text/policy}/oxfmtrc/oxfmtrc.rego +0 -0
- /package/{policy/text → rules/text/policy}/package_json/package_json.rego +0 -0
- /package/{policy/text → rules/text/policy}/vscode_extensions/vscode_extensions.rego +0 -0
- /package/{policy/text → rules/text/policy}/vscode_extensions/vscode_extensions_test.rego +0 -0
- /package/{policy/text → rules/text/policy}/vscode_settings/vscode_settings.rego +0 -0
- /package/{policy/text → rules/text/policy}/vscode_settings/vscode_settings_test.rego +0 -0
- /package/{policy/vue → rules/vue/policy}/package_json/package_json.rego +0 -0
- /package/{mdc → rules/vue}/vue.mdc +0 -0
|
@@ -22,10 +22,10 @@ TRANSCRIPT_PATH=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
|
|
|
22
22
|
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // "unknown"')
|
|
23
23
|
|
|
24
24
|
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
25
|
-
|
|
25
|
+
ADR_DIR="$PROJECT_ROOT/docs/adr"
|
|
26
26
|
LOG_DIR="$PROJECT_ROOT/.claude/hooks"
|
|
27
27
|
LOG="$LOG_DIR/capture-decisions.log"
|
|
28
|
-
mkdir -p "$
|
|
28
|
+
mkdir -p "$ADR_DIR" "$LOG_DIR"
|
|
29
29
|
|
|
30
30
|
log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
|
|
31
31
|
|
|
@@ -145,7 +145,7 @@ if ! printf '%s' "$RESPONSE_TRIMMED" | grep -q '^## '; then
|
|
|
145
145
|
fi
|
|
146
146
|
|
|
147
147
|
TS=$(date +%Y%m%d-%H%M%S)
|
|
148
|
-
OUT="$
|
|
148
|
+
OUT="$ADR_DIR/$TS-${SESSION_ID:0:8}.md"
|
|
149
149
|
{
|
|
150
150
|
printf -- '---\nsession: %s\ncaptured: %s\ntranscript: %s\n---\n\n' \
|
|
151
151
|
"$SESSION_ID" "$(date -Iseconds)" "$TRANSCRIPT_PATH"
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook: normalize ADR drafts in docs/adr/ via LLM.
|
|
3
|
+
#
|
|
4
|
+
# Triggers when number of draft files (with `session:` in YAML frontmatter)
|
|
5
|
+
# reaches ADR_NORMALIZE_THRESHOLD (default 30). Asks LLM to return a JSON list
|
|
6
|
+
# of operations (rewrite / delete / merge-into) and applies them to the working
|
|
7
|
+
# tree. Never invokes git — developer reviews via `git status` / `git diff`.
|
|
8
|
+
#
|
|
9
|
+
# LLM CLI selection (first available wins):
|
|
10
|
+
# 1. claude — `claude -p --model "$ADR_NORMALIZE_MODEL"` (default: sonnet)
|
|
11
|
+
# 2. cursor-agent — `cursor-agent -p --mode ask --output-format text --model …`
|
|
12
|
+
# (default: claude-4.6-sonnet-medium)
|
|
13
|
+
# neither — exit 0 silently
|
|
14
|
+
#
|
|
15
|
+
# Portable bash 3.2 (macOS /bin/bash): no `mapfile`, no associative arrays.
|
|
16
|
+
#
|
|
17
|
+
# Bundled with @nitra/cursor; project copy is auto-synced by the `adr` rule.
|
|
18
|
+
set -eu
|
|
19
|
+
set -o pipefail
|
|
20
|
+
|
|
21
|
+
if [ -n "${ADR_NORMALIZE_RUNNING:-}" ]; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
export ADR_NORMALIZE_RUNNING=1
|
|
25
|
+
|
|
26
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
27
|
+
ADR_DIR="$PROJECT_ROOT/docs/adr"
|
|
28
|
+
LOG_DIR="$PROJECT_ROOT/.claude/hooks"
|
|
29
|
+
LOG="$LOG_DIR/normalize-decisions.log"
|
|
30
|
+
STATE_FILE="$LOG_DIR/.normalize-state"
|
|
31
|
+
LOCK_FILE="$LOG_DIR/.normalize.lock"
|
|
32
|
+
mkdir -p "$LOG_DIR"
|
|
33
|
+
|
|
34
|
+
log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
|
|
35
|
+
|
|
36
|
+
# Skip if repo is mid-rebase / mid-merge — editing files now would tangle the user.
|
|
37
|
+
if [ -d "$PROJECT_ROOT/.git" ]; then
|
|
38
|
+
for marker in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-apply rebase-merge; do
|
|
39
|
+
if [ -e "$PROJECT_ROOT/.git/$marker" ]; then
|
|
40
|
+
log "skip: git is mid-$marker"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
done
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ ! -d "$ADR_DIR" ]; then
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Acquire lock if `flock` is available (Linux). macOS lacks flock by default —
|
|
51
|
+
# treat absence as "no concurrent runs expected" and skip locking.
|
|
52
|
+
if command -v flock >/dev/null 2>&1; then
|
|
53
|
+
exec 9>"$LOCK_FILE"
|
|
54
|
+
if ! flock -n 9; then
|
|
55
|
+
log "skip: another normalize run holds the lock"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Min interval between attempts — when LLM returns nothing for the batch, do not
|
|
61
|
+
# spin on every Stop event. Default 6 hours.
|
|
62
|
+
MIN_INTERVAL_HOURS="${ADR_NORMALIZE_MIN_INTERVAL_HOURS:-6}"
|
|
63
|
+
if [ -f "$STATE_FILE" ]; then
|
|
64
|
+
LAST_ATTEMPT=$(cat "$STATE_FILE" 2>/dev/null || printf '0')
|
|
65
|
+
NOW=$(date +%s)
|
|
66
|
+
ELAPSED=$(( NOW - LAST_ATTEMPT ))
|
|
67
|
+
MIN_SECS=$(( MIN_INTERVAL_HOURS * 3600 ))
|
|
68
|
+
if [ "$ELAPSED" -lt "$MIN_SECS" ]; then
|
|
69
|
+
log "skip: only $ELAPSED s since last attempt (min ${MIN_SECS}s)"
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
THRESHOLD="${ADR_NORMALIZE_THRESHOLD:-30}"
|
|
75
|
+
BATCH_SIZE="${ADR_NORMALIZE_BATCH:-30}"
|
|
76
|
+
DRY_RUN="${ADR_NORMALIZE_DRY:-0}"
|
|
77
|
+
|
|
78
|
+
# Detects whether a markdown file is a draft: has YAML frontmatter with `session:` field.
|
|
79
|
+
is_draft() {
|
|
80
|
+
awk '
|
|
81
|
+
NR==1 && /^---$/ { fm=1; next }
|
|
82
|
+
fm && /^---$/ { exit }
|
|
83
|
+
fm && /^session: / { found=1 }
|
|
84
|
+
END { exit !found }
|
|
85
|
+
' "$1" 2>/dev/null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/adr-normalize.XXXXXX")
|
|
89
|
+
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
90
|
+
|
|
91
|
+
DRAFTS_LIST="$TMP_DIR/drafts.txt"
|
|
92
|
+
CLEAN_LIST="$TMP_DIR/clean.txt"
|
|
93
|
+
BATCH_LIST="$TMP_DIR/batch.txt"
|
|
94
|
+
CLAIMED_SLUGS="$TMP_DIR/claimed.txt"
|
|
95
|
+
: > "$DRAFTS_LIST"
|
|
96
|
+
: > "$CLEAN_LIST"
|
|
97
|
+
: > "$CLAIMED_SLUGS"
|
|
98
|
+
|
|
99
|
+
# Find all draft files (recursive) under docs/adr/.
|
|
100
|
+
find "$ADR_DIR" -type f -name '*.md' 2>/dev/null | while IFS= read -r f; do
|
|
101
|
+
if is_draft "$f"; then
|
|
102
|
+
printf '%s\n' "$f"
|
|
103
|
+
fi
|
|
104
|
+
done | sort > "$DRAFTS_LIST"
|
|
105
|
+
|
|
106
|
+
DRAFT_COUNT=$(wc -l < "$DRAFTS_LIST" | tr -d ' ')
|
|
107
|
+
log "drafts found: $DRAFT_COUNT (threshold: $THRESHOLD)"
|
|
108
|
+
|
|
109
|
+
if [ "$DRAFT_COUNT" -lt "$THRESHOLD" ]; then
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
head -n "$BATCH_SIZE" "$DRAFTS_LIST" > "$BATCH_LIST"
|
|
114
|
+
BATCH_COUNT=$(wc -l < "$BATCH_LIST" | tr -d ' ')
|
|
115
|
+
log "batch size: $BATCH_COUNT"
|
|
116
|
+
|
|
117
|
+
# Find clean ADR files at root of docs/adr/ (no `session:`).
|
|
118
|
+
find "$ADR_DIR" -maxdepth 1 -type f -name '*.md' 2>/dev/null | while IFS= read -r f; do
|
|
119
|
+
if ! is_draft "$f"; then
|
|
120
|
+
basename "$f"
|
|
121
|
+
fi
|
|
122
|
+
done | sort > "$CLEAN_LIST"
|
|
123
|
+
|
|
124
|
+
# Build prompt input section.
|
|
125
|
+
INPUT_FILE="$TMP_DIR/input.md"
|
|
126
|
+
{
|
|
127
|
+
i=0
|
|
128
|
+
while IFS= read -r f; do
|
|
129
|
+
i=$(( i + 1 ))
|
|
130
|
+
idx=$(printf '%03d' "$i")
|
|
131
|
+
rel="${f#"$ADR_DIR/"}"
|
|
132
|
+
printf '\n[DRAFT-%s] %s\n' "$idx" "$rel"
|
|
133
|
+
cat "$f"
|
|
134
|
+
printf '\n[END DRAFT-%s]\n' "$idx"
|
|
135
|
+
done < "$BATCH_LIST"
|
|
136
|
+
} > "$INPUT_FILE"
|
|
137
|
+
|
|
138
|
+
CLEAN_SECTION_FILE="$TMP_DIR/clean-section.md"
|
|
139
|
+
: > "$CLEAN_SECTION_FILE"
|
|
140
|
+
if [ -s "$CLEAN_LIST" ]; then
|
|
141
|
+
{
|
|
142
|
+
printf '\nClean ADR files already in docs/adr/ (potential merge-into targets):\n'
|
|
143
|
+
while IFS= read -r c; do
|
|
144
|
+
printf '%s\n' "- $c"
|
|
145
|
+
done < "$CLEAN_LIST"
|
|
146
|
+
} > "$CLEAN_SECTION_FILE"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
PROMPT_HEADER=$(cat <<'EOF'
|
|
150
|
+
Ти нормалізуєш чернетки ADR/Runbook/Knowledge у `docs/adr/` репозиторію. Для кожного драфта обери одну з трьох операцій і поверни ЛИШЕ JSON-обʼєкт без markdown-обгортки, без передмови.
|
|
151
|
+
|
|
152
|
+
Схема відповіді:
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
"operations": [
|
|
156
|
+
{ "op": "delete", "file": "<basename>.md", "reason": "..." },
|
|
157
|
+
{ "op": "rewrite", "file": "<basename>.md", "slug": "<kebab-case-ukrainian>", "content": "<повний markdown файлу>" },
|
|
158
|
+
{ "op": "merge-into", "file": "<basename>.md", "target": "<slug>.md", "additions": "<markdown для дописування>" }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
Правила:
|
|
163
|
+
|
|
164
|
+
1. `delete` — драфт тривіальний / повністю покритий іншим існуючим clean-ADR-ом / порожній. Поясни короткою причиною українською.
|
|
165
|
+
|
|
166
|
+
2. `rewrite` — драфт має самостійну цінність. Повертай у `content` повний фінальний вміст файлу:
|
|
167
|
+
- Без YAML frontmatter (жодного `session:`, `captured:`, `transcript:`).
|
|
168
|
+
- Заголовок `# <Title>` українською.
|
|
169
|
+
- Один рядок `**Status:** Accepted` і один рядок `**Date:** YYYY-MM-DD` — дату беремо з поля `captured:` оригінальної чернетки (перші 10 символів ISO-дати).
|
|
170
|
+
- Далі розділи **Контекст**, **Рішення/Процедура/Факт**, **Обґрунтування**, **Розглянуті альтернативи**, **Зачіпає** — як у драфті, але причесані: цілісні речення, без скорочень, без слідів автогенерованого тегу типу `## Knowledge`.
|
|
171
|
+
- `slug` — kebab-case українською (наприклад `ланцюжок-запуску-abie`, `npm-publish-flow`). Без розширення `.md`. Літери малі, дозволено цифри, дефіс, кирилиця. Якщо тема технічна англійською (назва пакету, ключове слово) — лиши англійською без транслітерації.
|
|
172
|
+
|
|
173
|
+
3. `merge-into` — драфт повторює тему вже існуючого clean-файлу зі списку нижче. `target` — точна назва файлу зі списку (з `.md`). `additions` — лише новий зміст, який варто дописати в кінець target-файлу під підзаголовком `## Update YYYY-MM-DD` (date з `captured` драфта). Якщо нічого нового додати — використовуй `delete`.
|
|
174
|
+
|
|
175
|
+
Жорсткі обмеження:
|
|
176
|
+
|
|
177
|
+
- Поверни валідний JSON, нічого крім нього. Жодних code-fence, жодних коментарів.
|
|
178
|
+
- Кожен файл з вхідного списку має зʼявитися у `operations` рівно один раз.
|
|
179
|
+
- Слаги не повторювати між операціями того самого батча. Якщо дві чернетки про одну тему — одна `rewrite`, інша `merge-into target: <slug>.md` з тим самим slug-ом.
|
|
180
|
+
- Не вигадуй target, якого нема у списку clean-файлів.
|
|
181
|
+
|
|
182
|
+
Вхідні драфти і clean-список — нижче.
|
|
183
|
+
EOF
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
FULL_PROMPT_FILE="$TMP_DIR/prompt.md"
|
|
187
|
+
{
|
|
188
|
+
printf '%s\n' "$PROMPT_HEADER"
|
|
189
|
+
cat "$CLEAN_SECTION_FILE"
|
|
190
|
+
printf '\n=== DRAFTS ===\n'
|
|
191
|
+
cat "$INPUT_FILE"
|
|
192
|
+
} > "$FULL_PROMPT_FILE"
|
|
193
|
+
|
|
194
|
+
# Update state BEFORE calling LLM — even if LLM fails, we honor min-interval.
|
|
195
|
+
date +%s > "$STATE_FILE"
|
|
196
|
+
|
|
197
|
+
CLAUDE_MODEL="${ADR_NORMALIZE_MODEL:-sonnet}"
|
|
198
|
+
CURSOR_MODEL="${ADR_NORMALIZE_CURSOR_MODEL:-claude-4.6-sonnet-medium}"
|
|
199
|
+
|
|
200
|
+
RESPONSE_FILE="$TMP_DIR/response.txt"
|
|
201
|
+
|
|
202
|
+
if command -v claude >/dev/null 2>&1; then
|
|
203
|
+
log "using claude CLI (model: $CLAUDE_MODEL)"
|
|
204
|
+
claude -p --model "$CLAUDE_MODEL" < "$FULL_PROMPT_FILE" > "$RESPONSE_FILE" 2>>"$LOG" || true
|
|
205
|
+
elif command -v cursor-agent >/dev/null 2>&1; then
|
|
206
|
+
log "using cursor-agent CLI (model: $CURSOR_MODEL)"
|
|
207
|
+
FULL_PROMPT=$(cat "$FULL_PROMPT_FILE")
|
|
208
|
+
cursor-agent -p --mode ask --output-format text --model "$CURSOR_MODEL" -- "$FULL_PROMPT" > "$RESPONSE_FILE" 2>>"$LOG" || true
|
|
209
|
+
else
|
|
210
|
+
log "no LLM CLI found, skipping"
|
|
211
|
+
exit 0
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
if [ ! -s "$RESPONSE_FILE" ]; then
|
|
215
|
+
log "empty LLM response"
|
|
216
|
+
exit 0
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# Strip markdown code-fence lines and surrounding blank lines.
|
|
220
|
+
# Use awk to avoid backtick/end-anchor lint false positives in sed regex.
|
|
221
|
+
RESPONSE_CLEAN_FILE="$TMP_DIR/response-clean.json"
|
|
222
|
+
awk '
|
|
223
|
+
/^[[:space:]]*```/ { next }
|
|
224
|
+
started || /[^[:space:]]/ { started = 1; print }
|
|
225
|
+
' "$RESPONSE_FILE" > "$RESPONSE_CLEAN_FILE"
|
|
226
|
+
|
|
227
|
+
# Validate JSON.
|
|
228
|
+
if ! jq -e '.operations | type == "array"' "$RESPONSE_CLEAN_FILE" >/dev/null 2>&1; then
|
|
229
|
+
HEAD200=$(head -c 200 "$RESPONSE_CLEAN_FILE" | tr '\n' ' ')
|
|
230
|
+
log "invalid JSON response (first 200 chars): $HEAD200"
|
|
231
|
+
exit 0
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
OP_COUNT=$(jq '.operations | length' "$RESPONSE_CLEAN_FILE")
|
|
235
|
+
log "operations parsed: $OP_COUNT"
|
|
236
|
+
|
|
237
|
+
if [ "$DRY_RUN" = "1" ]; then
|
|
238
|
+
log "DRY RUN — would apply $OP_COUNT operations:"
|
|
239
|
+
jq -r '.operations[] | " " + .op + " " + .file + (if .slug then " → " + .slug else "" end) + (if .target then " → " + .target else "" end)' \
|
|
240
|
+
"$RESPONSE_CLEAN_FILE" >> "$LOG"
|
|
241
|
+
exit 0
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# Resolve unique target path for slug — appends -2, -3 on collision.
|
|
245
|
+
# Tracks claims via CLAIMED_SLUGS file (one slug per line).
|
|
246
|
+
resolve_unique_slug_path() {
|
|
247
|
+
slug="$1"
|
|
248
|
+
base="$ADR_DIR/${slug}.md"
|
|
249
|
+
if [ ! -e "$base" ] && ! grep -Fxq "$slug" "$CLAIMED_SLUGS" 2>/dev/null; then
|
|
250
|
+
printf '%s\n' "$slug" >> "$CLAIMED_SLUGS"
|
|
251
|
+
printf '%s\n' "$base"
|
|
252
|
+
return
|
|
253
|
+
fi
|
|
254
|
+
n=2
|
|
255
|
+
while :; do
|
|
256
|
+
cand="$ADR_DIR/${slug}-${n}.md"
|
|
257
|
+
key="${slug}-${n}"
|
|
258
|
+
if [ ! -e "$cand" ] && ! grep -Fxq "$key" "$CLAIMED_SLUGS" 2>/dev/null; then
|
|
259
|
+
printf '%s\n' "$key" >> "$CLAIMED_SLUGS"
|
|
260
|
+
printf '%s\n' "$cand"
|
|
261
|
+
return
|
|
262
|
+
fi
|
|
263
|
+
n=$(( n + 1 ))
|
|
264
|
+
done
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
APPLIED=0
|
|
268
|
+
SKIPPED=0
|
|
269
|
+
|
|
270
|
+
jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
|
|
271
|
+
OP=$(printf '%s' "$op_json" | jq -r '.op // empty')
|
|
272
|
+
FILE=$(printf '%s' "$op_json" | jq -r '.file // empty')
|
|
273
|
+
SRC_PATH="$ADR_DIR/$FILE"
|
|
274
|
+
|
|
275
|
+
if [ -z "$OP" ] || [ -z "$FILE" ]; then
|
|
276
|
+
log "skip: malformed op (missing op/file)"
|
|
277
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
278
|
+
continue
|
|
279
|
+
fi
|
|
280
|
+
case "$FILE" in
|
|
281
|
+
*/*|.*)
|
|
282
|
+
log "skip: refusing path-like file '$FILE'"
|
|
283
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
284
|
+
continue
|
|
285
|
+
;;
|
|
286
|
+
esac
|
|
287
|
+
|
|
288
|
+
# Resolve nested batch files by basename if not at docs/adr/ root.
|
|
289
|
+
if [ ! -f "$SRC_PATH" ]; then
|
|
290
|
+
while IFS= read -r bf; do
|
|
291
|
+
bn=$(basename "$bf")
|
|
292
|
+
if [ "$bn" = "$FILE" ]; then
|
|
293
|
+
SRC_PATH="$bf"
|
|
294
|
+
break
|
|
295
|
+
fi
|
|
296
|
+
done < "$BATCH_LIST"
|
|
297
|
+
fi
|
|
298
|
+
if [ ! -f "$SRC_PATH" ]; then
|
|
299
|
+
log "skip: source missing '$FILE'"
|
|
300
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
301
|
+
continue
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
case "$OP" in
|
|
305
|
+
delete)
|
|
306
|
+
REASON=$(printf '%s' "$op_json" | jq -r '.reason // ""')
|
|
307
|
+
rm -- "$SRC_PATH"
|
|
308
|
+
log "delete: $FILE — $REASON"
|
|
309
|
+
APPLIED=$(( APPLIED + 1 ))
|
|
310
|
+
;;
|
|
311
|
+
rewrite)
|
|
312
|
+
SLUG=$(printf '%s' "$op_json" | jq -r '.slug // empty')
|
|
313
|
+
CONTENT=$(printf '%s' "$op_json" | jq -r '.content // empty')
|
|
314
|
+
if [ -z "$SLUG" ] || [ -z "$CONTENT" ]; then
|
|
315
|
+
log "skip rewrite: missing slug or content for '$FILE'"
|
|
316
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
317
|
+
continue
|
|
318
|
+
fi
|
|
319
|
+
case "$SLUG" in
|
|
320
|
+
*/*|.*)
|
|
321
|
+
log "skip rewrite: refusing path-like slug '$SLUG'"
|
|
322
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
323
|
+
continue
|
|
324
|
+
;;
|
|
325
|
+
esac
|
|
326
|
+
DEST_PATH=$(resolve_unique_slug_path "$SLUG")
|
|
327
|
+
printf '%s\n' "$CONTENT" > "$DEST_PATH"
|
|
328
|
+
rm -- "$SRC_PATH"
|
|
329
|
+
log "rewrite: $FILE → $(basename "$DEST_PATH")"
|
|
330
|
+
APPLIED=$(( APPLIED + 1 ))
|
|
331
|
+
;;
|
|
332
|
+
merge-into)
|
|
333
|
+
TARGET=$(printf '%s' "$op_json" | jq -r '.target // empty')
|
|
334
|
+
ADDITIONS=$(printf '%s' "$op_json" | jq -r '.additions // empty')
|
|
335
|
+
if [ -z "$TARGET" ] || [ -z "$ADDITIONS" ]; then
|
|
336
|
+
log "skip merge-into: missing target or additions for '$FILE'"
|
|
337
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
338
|
+
continue
|
|
339
|
+
fi
|
|
340
|
+
case "$TARGET" in
|
|
341
|
+
*/*|.*)
|
|
342
|
+
log "skip merge-into: refusing path-like target '$TARGET'"
|
|
343
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
344
|
+
continue
|
|
345
|
+
;;
|
|
346
|
+
esac
|
|
347
|
+
TARGET_PATH="$ADR_DIR/$TARGET"
|
|
348
|
+
if [ ! -f "$TARGET_PATH" ]; then
|
|
349
|
+
log "skip merge-into: target '$TARGET' missing"
|
|
350
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
351
|
+
continue
|
|
352
|
+
fi
|
|
353
|
+
if is_draft "$TARGET_PATH"; then
|
|
354
|
+
log "skip merge-into: target '$TARGET' is itself a draft"
|
|
355
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
356
|
+
continue
|
|
357
|
+
fi
|
|
358
|
+
printf '\n%s\n' "$ADDITIONS" >> "$TARGET_PATH"
|
|
359
|
+
rm -- "$SRC_PATH"
|
|
360
|
+
log "merge-into: $FILE → $TARGET"
|
|
361
|
+
APPLIED=$(( APPLIED + 1 ))
|
|
362
|
+
;;
|
|
363
|
+
*)
|
|
364
|
+
log "skip: unknown op '$OP' for '$FILE'"
|
|
365
|
+
SKIPPED=$(( SKIPPED + 1 ))
|
|
366
|
+
;;
|
|
367
|
+
esac
|
|
368
|
+
done
|
|
369
|
+
|
|
370
|
+
log "done"
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.10.0] - 2026-05-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Правило `adr` — фаза 2 (Normalize).** Новий Stop-hook `.claude/hooks/normalize-decisions.sh` батчево нормалізує ADR-чернетки через LLM: коли кількість файлів з `session:` у frontmatter досягає `ADR_NORMALIZE_THRESHOLD` (default 30), бере до `ADR_NORMALIZE_BATCH` найстарших, отримує JSON-операції (`rewrite` / `delete` / `merge-into`) і застосовує їх до робочого дерева. **Жодних git-операцій** — розробник дивиться `git status`/`git diff` і вирішує сам. Recursion guard `ADR_NORMALIZE_RUNNING=1`, мінімальний інтервал між спробами `ADR_NORMALIZE_MIN_INTERVAL_HOURS=6`, lock-файл, skip при mid-rebase/mid-merge, `ADR_NORMALIZE_DRY=1` для dry-run. Slug-стиль — kebab-case українською; дата у фінальному ADR береться з `captured` чернетки.
|
|
12
|
+
- **Skill `adr-normalize`** (slash-команда `/n-adr-normalize`) — ручний запуск normalize поза порогом і поза Stop-hook (виставляє `ADR_NORMALIZE_THRESHOLD=0` і `ADR_NORMALIZE_MIN_INTERVAL_HOURS=0`, корисно для dry-run або разової чистки). Авто-додається при `adr` у `rules`.
|
|
13
|
+
- **`sync-claude-config`**: експортовано `ADR_NORMALIZE_HOOK_COMMAND_MARKER` і функцію `syncAdrNormalizeHookScript`; managed-група normalize додається до `hooks.Stop` поряд з capture-групою (`async: true`, `timeout: 600`) при `"adr"` у `rules`. Маркер `.claude/hooks/normalize-decisions.sh` додано в `MANAGED_HOOK_COMMAND_MARKERS`.
|
|
14
|
+
- **Rego-перевірка `adr.settings_json`** тепер вимагає Stop-hook групу і для capture, і для normalize; **`adr.settings_local_json`** забороняє дублі обох хуків.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **`capture-decisions.sh` тепер пише чернетки напряму в `docs/adr/<timestamp>-<sid>.md`** (раніше — у `docs/adr/_inbox/`). Сам каталог `_inbox/` більше не створюється, але `normalize-decisions.sh` бачить його рекурсивно — старі чернетки з `_inbox/` поступово розчищаються нормалізацією. Можна також одноразово `git mv docs/adr/_inbox/*.md docs/adr/` і прибрати порожній каталог.
|
|
19
|
+
- **Правило `adr` (`npm/rules/adr/adr.mdc`)**: повне переписування під дві фази (capture + normalize). Видалено згадки `_inbox/`. Версія `version: '2.0'`.
|
|
20
|
+
- **`npm/rules/adr/js/check.mjs`**: перевірка обох hook-скриптів (canonicity), обох log-файлів у `.gitignore`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- `npm/rules/adr/js/check.{mjs,test.mjs}`: виправлено `BUNDLED_HOOKS_DIR` (після phase 3 co-location шлях `'..'` указував у `npm/rules/adr/.claude-template/`, потрібно `'../../..'` — до `npm/.claude-template/`).
|
|
25
|
+
|
|
26
|
+
## [1.9.23] - 2026-05-14
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `npm/package.json#files`: додано негативні glob-патерни `!**/*.test.mjs`, `!**/test-helpers.mjs`, `!**/fixtures/**`, щоб після переїзду тестів у `rules/<rule>/js/`, `scripts/`, `scripts/utils/` вони не потрапляли в опубліковану npm-таrball (вимагає правило `npm-module`).
|
|
31
|
+
- `npm/package.json#devDependencies`: додано `@nitra/cursor: ^1.9.22` (auto-fill від `ensure-nitra-cursor-dev-dependencies.mjs`).
|
|
32
|
+
|
|
33
|
+
## [1.9.22] - 2026-05-14
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Rule-centric структура пакета.** Кожне правило тепер живе в одній директорії `npm/rules/{rule}/` з усіма своїми артефактами: `{rule}.mdc`, `auto.md` (умова автоактивації), `policy/` (rego-поліси), `js/` (check.mjs + опційні run/lint + co-located *.test.mjs + fixtures/). Видалив каталог правила — правило зникло без слідів у `bin/auto-rules.md`, `npm/policy/`, `npm/scripts/`. Дзеркальна структура для скілів у `npm/skills/{skill}/` (SKILL.md + auto.md + js/).
|
|
38
|
+
- **Тести співрозташовуються з джерелами.** ~50 файлів з `npm/tests/` переїхали в `npm/rules/{rule}/js/*.test.mjs` (тести правил), `npm/scripts/*.test.mjs` (тести інфраструктури), `npm/scripts/utils/*.test.mjs` (тести утиліт). `tests/helpers.mjs` → `scripts/utils/test-helpers.mjs`. `npm/tests/` залишається тільки для 3 крос-правильних інтеграційних тестів.
|
|
39
|
+
- **`bin/n-cursor.js`**: `BUNDLED_MDC_DIR` → `BUNDLED_RULES_DIR`. `discoverBundledRuleNames` і `discoverCheckScripts` тепер обходять підкаталоги `rules/` замість файлів у `mdc/` чи `check-*.mjs` у `scripts/`. Резолвер check-скриптів: `rules/{rule}/js/check.mjs`. `readBundledRuleContent` читає `rules/{rule}/{rule}.mdc`.
|
|
40
|
+
- **`scripts/utils/run-conftest-batch.mjs` та `scripts/lint-conftest.mjs`**: шляхи до rego-полісі — `rules/{rule}/policy/{name}/` (замість `policy/{rule}/{name}/`). Snake_case `policyDirRel` у JS-call sites замінено на kebab-case.
|
|
41
|
+
- **`npm/package.json#files`**: `mdc` і `policy` видалено, додано `rules`. `scripts.test`: `bun test tests` → `bun test` (рекурсивний пошук `*.test.mjs`).
|
|
42
|
+
- **`.cursor/rules/scripts.mdc`** (v1.5): додано секцію «Структура правила» з документацією rule-centric layout для майбутніх правил. Path-references у `npm/CLAUDE.md` оновлено.
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
|
|
46
|
+
- `npm/mdc/` (24 файли) — вміст переїхав у `npm/rules/{rule}/{rule}.mdc`.
|
|
47
|
+
- `npm/policy/` (24 каталоги) — вміст переїхав у `npm/rules/{rule}/policy/`.
|
|
48
|
+
- `npm/bin/auto-rules.md`, `npm/bin/auto-skills.md` — замінено на per-rule і per-skill `auto.md` в кожному каталозі.
|
|
49
|
+
|
|
7
50
|
## [1.9.20] - 2026-05-14
|
|
8
51
|
|
|
9
52
|
### Added
|