@svayam-opensource/prj 0.5.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 +123 -0
- package/agent/harness-manifest.yaml +225 -0
- package/agent/session-protocol.md +116 -0
- package/bin/prj +21 -0
- package/package.json +41 -0
- package/prj +2381 -0
- package/scripts/add-repo.sh +126 -0
- package/scripts/cancel.sh +157 -0
- package/scripts/close-knowledge.sh +250 -0
- package/scripts/close-project.sh +233 -0
- package/scripts/create-task.sh +226 -0
- package/scripts/install-deps.sh +292 -0
- package/scripts/join.sh +89 -0
- package/scripts/lib.sh +841 -0
- package/scripts/merge-task.sh +163 -0
- package/scripts/onboard-repo.sh +275 -0
- package/scripts/pause.sh +80 -0
- package/scripts/project-access.sh +34 -0
- package/scripts/propose-knowledge.sh +168 -0
- package/scripts/release-to-public.sh +185 -0
- package/scripts/render-harness.sh +151 -0
- package/scripts/resume.sh +103 -0
- package/scripts/seed.sh +774 -0
- package/scripts/sync-from-publish.sh +193 -0
- package/scripts/sync.sh +90 -0
- package/scripts/test-merge.sh +100 -0
- package/scripts/validate/check_knowledge.py +158 -0
- package/scripts/validate/check_privacy.py +211 -0
- package/scripts/validate/check_protocol.py +117 -0
- package/scripts/validate/check_secrets.py +175 -0
- package/scripts/validate/run.py +391 -0
- package/setup.sh +529 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Script: sync-from-publish
|
|
3
|
+
# Purpose: Pull universal framework updates from `publish` into `main`,
|
|
4
|
+
# preserving main's private overlay (org-config.yaml, registry.yaml,
|
|
5
|
+
# projects/). Direction A: framework files contain no placeholders,
|
|
6
|
+
# so this is a plain merge — no re-substitution step.
|
|
7
|
+
#
|
|
8
|
+
# Usage: bash scripts/sync-from-publish.sh [--dry-run] [--no-push]
|
|
9
|
+
#
|
|
10
|
+
# Flags:
|
|
11
|
+
# --dry-run Show what would be merged without making any changes.
|
|
12
|
+
# --no-push Do the merge + validation locally; do not push to origin/main.
|
|
13
|
+
#
|
|
14
|
+
# Privacy: NEVER syncs main → publish. One-way (publish → main).
|
|
15
|
+
#
|
|
16
|
+
# Strategy:
|
|
17
|
+
# 1. Verify on main, clean, fast-forwardable from origin.
|
|
18
|
+
# 2. Show commits to be merged; confirm (skip if --dry-run).
|
|
19
|
+
# 3. Create ephemeral test branch test-sync-from-publish from main.
|
|
20
|
+
# 4. Merge publish with -X theirs (auto-prefer publish for content conflicts).
|
|
21
|
+
# 5. Restore main's private overlay (org-config.yaml, registry.yaml, projects/)
|
|
22
|
+
# from main's pre-merge state.
|
|
23
|
+
# 6. Run validators — fail if any placeholders leaked (framework files must
|
|
24
|
+
# stay placeholder-free; validator's placeholder check is always-on).
|
|
25
|
+
# 7. On pass: fast-forward main to test branch tip, push, delete test branch.
|
|
26
|
+
# 8. On fail: discard test branch, restore main, exit non-zero.
|
|
27
|
+
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
source "$(dirname "$0")/lib.sh"
|
|
30
|
+
load_config
|
|
31
|
+
|
|
32
|
+
DRY_RUN=false
|
|
33
|
+
NO_PUSH=false
|
|
34
|
+
|
|
35
|
+
for arg in "$@"; do
|
|
36
|
+
case "$arg" in
|
|
37
|
+
--dry-run) DRY_RUN=true ;;
|
|
38
|
+
--no-push) NO_PUSH=true ;;
|
|
39
|
+
--allow-downgrade) export ALLOW_DOWNGRADE=true ;;
|
|
40
|
+
-h|--help)
|
|
41
|
+
grep '^# ' "$0" | sed 's/^# //;s/^#//'
|
|
42
|
+
exit 0
|
|
43
|
+
;;
|
|
44
|
+
*) hard_stop "Unknown flag: $arg (see --help)" ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
cd "$REPO_ROOT"
|
|
49
|
+
|
|
50
|
+
# ── Pre-conditions ────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
53
|
+
[[ "$CURRENT_BRANCH" == "$DEFAULT_BRANCH" ]] \
|
|
54
|
+
|| hard_stop "Run from '$DEFAULT_BRANCH' (currently on '$CURRENT_BRANCH')."
|
|
55
|
+
|
|
56
|
+
[[ -z "$(git status --porcelain)" ]] \
|
|
57
|
+
|| hard_stop "Uncommitted changes present. Stash or commit first."
|
|
58
|
+
|
|
59
|
+
# ── Fetch latest ──────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
echo "=== sync-from-publish"
|
|
62
|
+
$DRY_RUN && echo " (dry-run mode — no changes will be made)"
|
|
63
|
+
$NO_PUSH && echo " (--no-push — will merge locally but not push)"
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
info "Fetching latest..."
|
|
67
|
+
git fetch origin "$DEFAULT_BRANCH"
|
|
68
|
+
git fetch origin publish
|
|
69
|
+
|
|
70
|
+
if ! git pull --ff-only origin "$DEFAULT_BRANCH"; then
|
|
71
|
+
hard_stop "Cannot fast-forward local '$DEFAULT_BRANCH' from origin."
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# ── Identify commits to sync ──────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
COMMITS_TO_SYNC=$(git rev-list --count "$DEFAULT_BRANCH..origin/publish" 2>/dev/null || echo 0)
|
|
77
|
+
|
|
78
|
+
if [[ "$COMMITS_TO_SYNC" -eq 0 ]]; then
|
|
79
|
+
echo "✓ '$DEFAULT_BRANCH' is already up to date with publish."
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# ── Framework version guard: never let an OLDER publish overwrite main ────────
|
|
84
|
+
PUB_VER="$(git show origin/publish:framework/VERSION 2>/dev/null | tr -d '[:space:]')"
|
|
85
|
+
CUR_VER="$(git show "$DEFAULT_BRANCH:framework/VERSION" 2>/dev/null | tr -d '[:space:]')"
|
|
86
|
+
[[ -n "$PUB_VER" ]] && info "framework: publish v$PUB_VER → $DEFAULT_BRANCH v$CUR_VER"
|
|
87
|
+
assert_no_framework_downgrade "$PUB_VER" "$CUR_VER" "sync-from-publish"
|
|
88
|
+
|
|
89
|
+
echo ""
|
|
90
|
+
echo "Commits on publish not yet on $DEFAULT_BRANCH ($COMMITS_TO_SYNC total):"
|
|
91
|
+
git log --oneline "$DEFAULT_BRANCH..origin/publish" | head -30
|
|
92
|
+
echo ""
|
|
93
|
+
|
|
94
|
+
if $DRY_RUN; then
|
|
95
|
+
echo ""
|
|
96
|
+
info "Dry-run: would merge the above commits into $DEFAULT_BRANCH using -X theirs,"
|
|
97
|
+
info " restore private overlay (org-config.yaml, registry.yaml, projects/),"
|
|
98
|
+
info " run validators,"
|
|
99
|
+
info " and (unless --no-push) push to origin/$DEFAULT_BRANCH."
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
confirm "Proceed with merge?"
|
|
104
|
+
|
|
105
|
+
# ── Test branch + merge ───────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
TEST_BRANCH="test-sync-from-publish"
|
|
108
|
+
ORIGINAL_SHA=$(git rev-parse HEAD)
|
|
109
|
+
|
|
110
|
+
cleanup_on_fail() {
|
|
111
|
+
echo ""
|
|
112
|
+
warn "Sync failed — restoring '$DEFAULT_BRANCH' to pre-sync state."
|
|
113
|
+
git merge --abort 2>/dev/null || true
|
|
114
|
+
git checkout "$DEFAULT_BRANCH" 2>/dev/null || true
|
|
115
|
+
git reset --hard "$ORIGINAL_SHA" 2>/dev/null || true
|
|
116
|
+
git branch -D "$TEST_BRANCH" 2>/dev/null || true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if git rev-parse --verify "$TEST_BRANCH" &>/dev/null; then
|
|
120
|
+
warn "Stale test branch '$TEST_BRANCH' exists — deleting."
|
|
121
|
+
git branch -D "$TEST_BRANCH"
|
|
122
|
+
fi
|
|
123
|
+
git checkout -b "$TEST_BRANCH"
|
|
124
|
+
|
|
125
|
+
info "Merging publish with -X theirs (auto-prefers publish for conflicts)..."
|
|
126
|
+
if ! git merge -X theirs --no-edit -m "sync: publish → $DEFAULT_BRANCH" origin/publish; then
|
|
127
|
+
cleanup_on_fail
|
|
128
|
+
hard_stop "Merge failed despite -X theirs. Manual intervention required."
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# ── Restore private overlay from pre-merge main ───────────────────────────────
|
|
132
|
+
|
|
133
|
+
info "Restoring private overlay (org-config.yaml, registry.yaml, projects/)..."
|
|
134
|
+
|
|
135
|
+
# HEAD^1 is the first parent (pre-merge main); HEAD^2 is publish's tip.
|
|
136
|
+
# We want main's pre-merge content for these specific paths.
|
|
137
|
+
PRIVATE_PATHS=("org-config.yaml" "registry.yaml")
|
|
138
|
+
for path in "${PRIVATE_PATHS[@]}"; do
|
|
139
|
+
if git ls-tree HEAD^1 -- "$path" &>/dev/null; then
|
|
140
|
+
git checkout HEAD^1 -- "$path"
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
|
|
144
|
+
# projects/ may exist on main only (publish has empty projects/)
|
|
145
|
+
if git ls-tree HEAD^1 -- projects/ &>/dev/null; then
|
|
146
|
+
# Remove any projects content that came from publish, then restore main's
|
|
147
|
+
git rm -rf --cached projects/ &>/dev/null || true
|
|
148
|
+
rm -rf projects
|
|
149
|
+
git checkout HEAD^1 -- projects/
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# Amend the merge commit with the overlay restorations.
|
|
153
|
+
if [[ -n "$(git diff --cached --name-only)" ]]; then
|
|
154
|
+
git commit --amend --no-edit
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# ── Validate the merged tree ──────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
VALIDATOR="$REPO_ROOT/scripts/validate/run.py"
|
|
160
|
+
if [[ -x "$VALIDATOR" ]]; then
|
|
161
|
+
echo ""
|
|
162
|
+
info "Running validators against merged tree..."
|
|
163
|
+
echo ""
|
|
164
|
+
if ! python3 "$VALIDATOR" "$REPO_ROOT"; then
|
|
165
|
+
echo ""
|
|
166
|
+
cleanup_on_fail
|
|
167
|
+
hard_stop "Validation FAILED — sync rolled back. '$DEFAULT_BRANCH' is unchanged."
|
|
168
|
+
fi
|
|
169
|
+
else
|
|
170
|
+
warn "Validator not found at $VALIDATOR — skipping post-merge validation."
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# ── Promote test branch to main and clean up ──────────────────────────────────
|
|
174
|
+
|
|
175
|
+
echo ""
|
|
176
|
+
info "Validation passed. Promoting to '$DEFAULT_BRANCH'..."
|
|
177
|
+
git checkout "$DEFAULT_BRANCH"
|
|
178
|
+
git merge --ff-only "$TEST_BRANCH"
|
|
179
|
+
git branch -d "$TEST_BRANCH"
|
|
180
|
+
|
|
181
|
+
# ── Push (unless --no-push) ───────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
if $NO_PUSH; then
|
|
184
|
+
echo ""
|
|
185
|
+
info "✓ Sync complete locally. NOT pushed (--no-push)."
|
|
186
|
+
info " When ready: git push origin $DEFAULT_BRANCH"
|
|
187
|
+
else
|
|
188
|
+
echo ""
|
|
189
|
+
info "Pushing to origin/$DEFAULT_BRANCH..."
|
|
190
|
+
git push origin "$DEFAULT_BRANCH"
|
|
191
|
+
echo ""
|
|
192
|
+
info "✓ Sync complete. '$DEFAULT_BRANCH' updated and pushed."
|
|
193
|
+
fi
|
package/scripts/sync.sh
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Script: sync
|
|
3
|
+
# Purpose: Merges latest DEFAULT_BRANCH/base into active project branch on demand.
|
|
4
|
+
# Use mid-project to stay current without pausing/resuming.
|
|
5
|
+
# Usage: bash sync.sh <project_id>
|
|
6
|
+
# Compliance: C03 — encouraged but not mandatory (POL-122)
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
source "$(dirname "$0")/lib.sh"
|
|
10
|
+
load_config
|
|
11
|
+
|
|
12
|
+
# ── Inputs ────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
PROJECT_ID="${1:-}"
|
|
15
|
+
[[ -n "$PROJECT_ID" ]] || hard_stop "Usage: $0 <project_id>"
|
|
16
|
+
|
|
17
|
+
echo "=== sync: $PROJECT_ID"
|
|
18
|
+
echo ""
|
|
19
|
+
|
|
20
|
+
PROJECT_YAML=$(get_project_yaml "$PROJECT_ID")
|
|
21
|
+
check_project_exists "$PROJECT_ID"
|
|
22
|
+
|
|
23
|
+
# ── Pre-conditions ────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
require_project_status "$PROJECT_YAML" "active"
|
|
26
|
+
|
|
27
|
+
CURRENT_USER=$(git config user.email 2>/dev/null || echo "")
|
|
28
|
+
ASSIGNED_TO=$(yaml_get "$PROJECT_YAML" "assigned_to") # display/audit cache
|
|
29
|
+
GH_PROJECT=$(yaml_get "$PROJECT_YAML" "github_project")
|
|
30
|
+
is_authorized_for_project "$GH_PROJECT" "$ASSIGNED_TO" \
|
|
31
|
+
|| hard_stop "Not authorized to sync — '$CURRENT_USER' needs write access to the project's GitHub Project ($GH_PROJECT)."
|
|
32
|
+
|
|
33
|
+
BRANCH=$(project_branch_for_id "$PROJECT_ID")
|
|
34
|
+
|
|
35
|
+
echo "Checking for uncommitted changes..."
|
|
36
|
+
check_clean "$REPO_ROOT"
|
|
37
|
+
while IFS= read -r repo_url; do
|
|
38
|
+
REPO_DIR="$(repo_clone_dir "$PROJECT_ID" "$(get_repo_name "$repo_url")")"
|
|
39
|
+
[[ -e "$REPO_DIR/.git" ]] && check_clean "$REPO_DIR"
|
|
40
|
+
done < <(get_project_repos "$PROJECT_YAML")
|
|
41
|
+
info "All repos are clean."
|
|
42
|
+
echo ""
|
|
43
|
+
|
|
44
|
+
# ── Sync workspace repo ───────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
echo "Syncing workspace repo: $DEFAULT_BRANCH → $BRANCH..."
|
|
47
|
+
cd "$REPO_ROOT"
|
|
48
|
+
git fetch origin "$DEFAULT_BRANCH"
|
|
49
|
+
git checkout "$BRANCH"
|
|
50
|
+
if ! git merge --no-edit "origin/$DEFAULT_BRANCH" 2>/dev/null; then
|
|
51
|
+
echo ""
|
|
52
|
+
echo "MERGE CONFLICT: $DEFAULT_BRANCH → $BRANCH in workspace repo."
|
|
53
|
+
echo "Resolve conflicts manually, commit, then re-run: bash sync.sh $PROJECT_ID"
|
|
54
|
+
exit 2
|
|
55
|
+
fi
|
|
56
|
+
git push origin "$BRANCH"
|
|
57
|
+
info "Workspace repo synced."
|
|
58
|
+
|
|
59
|
+
# ── Sync each code repo ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
while IFS= read -r repo_url; do
|
|
62
|
+
REPO_NAME=$(get_repo_name "$repo_url")
|
|
63
|
+
REPO_DIR="$(repo_clone_dir "$PROJECT_ID" "$REPO_NAME")"
|
|
64
|
+
REPO_BASE=$(get_repo_base_branch "$PROJECT_YAML" "$repo_url")
|
|
65
|
+
|
|
66
|
+
if [[ ! -e "$REPO_DIR/.git" ]]; then
|
|
67
|
+
warn "Repo $REPO_NAME not cloned locally — skipping sync."
|
|
68
|
+
continue
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo "Syncing $REPO_NAME: $REPO_BASE → $BRANCH..."
|
|
72
|
+
git -C "$REPO_DIR" fetch origin "$REPO_BASE"
|
|
73
|
+
git -C "$REPO_DIR" checkout "$BRANCH"
|
|
74
|
+
if ! git -C "$REPO_DIR" merge --no-edit "origin/$REPO_BASE" 2>/dev/null; then
|
|
75
|
+
echo ""
|
|
76
|
+
echo "MERGE CONFLICT: $REPO_BASE → $BRANCH in $REPO_NAME."
|
|
77
|
+
echo "Resolve conflicts manually, commit, then re-run: bash sync.sh $PROJECT_ID"
|
|
78
|
+
exit 2
|
|
79
|
+
fi
|
|
80
|
+
git -C "$REPO_DIR" push origin "$BRANCH"
|
|
81
|
+
info "$REPO_NAME synced."
|
|
82
|
+
done < <(get_project_repos "$PROJECT_YAML")
|
|
83
|
+
|
|
84
|
+
echo ""
|
|
85
|
+
echo "=== Sync complete. All project branches are current."
|
|
86
|
+
echo ""
|
|
87
|
+
echo "[ C03 ] Reload knowledge layers before continuing work:"
|
|
88
|
+
echo " 1. $WORKSPACE_REPO/knowledge/"
|
|
89
|
+
echo " 2. $WORKSPACE_REPO/projects/$PROJECT_ID/knowledge/"
|
|
90
|
+
echo " 3. <repo>/knowledge/ for each repo"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Script: test-merge
|
|
3
|
+
# Purpose: Pre-merge gate for the workspace repo's default branch.
|
|
4
|
+
# Validates that merging <source-branch> will not violate schema
|
|
5
|
+
# or invariants. On pass, fast-forwards local default to the
|
|
6
|
+
# merged tip — caller pushes. On fail, leaves local default
|
|
7
|
+
# unchanged.
|
|
8
|
+
#
|
|
9
|
+
# Usage: bash scripts/test-merge.sh <source-branch>
|
|
10
|
+
#
|
|
11
|
+
# Behavior:
|
|
12
|
+
# 1. Sync local $DEFAULT_BRANCH with remote (only legitimate remote read)
|
|
13
|
+
# 2. Create ephemeral test branch test-merge/<source> from $DEFAULT_BRANCH
|
|
14
|
+
# 3. Merge <source> into test branch (no push)
|
|
15
|
+
# 4. Run scripts/validate/run.py against the merged tree
|
|
16
|
+
# 5a. Pass: fast-forward $DEFAULT_BRANCH to test-merge tip; delete test branch
|
|
17
|
+
# 5b. Fail: discard test branch; $DEFAULT_BRANCH untouched; exit non-zero
|
|
18
|
+
#
|
|
19
|
+
# This script is the foundation of the test-merge gate strategy. See:
|
|
20
|
+
# knowledge/guidance/test-merge-gate.md (TODO)
|
|
21
|
+
#
|
|
22
|
+
# Compliance: C01 — workspace integrity gate
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
source "$(dirname "$0")/lib.sh"
|
|
26
|
+
load_config
|
|
27
|
+
|
|
28
|
+
SOURCE_BRANCH="${1:-}"
|
|
29
|
+
[[ -n "$SOURCE_BRANCH" ]] || hard_stop "Usage: $0 <source-branch>"
|
|
30
|
+
|
|
31
|
+
cd "$REPO_ROOT"
|
|
32
|
+
|
|
33
|
+
ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
34
|
+
TEST_BRANCH="test-merge/$SOURCE_BRANCH"
|
|
35
|
+
VALIDATOR="$REPO_ROOT/scripts/validate/run.py"
|
|
36
|
+
|
|
37
|
+
[[ -x "$VALIDATOR" ]] || hard_stop "Validator not found or not executable: $VALIDATOR"
|
|
38
|
+
|
|
39
|
+
# Verify source branch exists locally or on remote
|
|
40
|
+
if ! git rev-parse --verify "$SOURCE_BRANCH" &>/dev/null; then
|
|
41
|
+
if git ls-remote --exit-code origin "$SOURCE_BRANCH" &>/dev/null; then
|
|
42
|
+
info "Fetching $SOURCE_BRANCH from origin..."
|
|
43
|
+
git fetch origin "$SOURCE_BRANCH:$SOURCE_BRANCH" 2>/dev/null \
|
|
44
|
+
|| git fetch origin "$SOURCE_BRANCH"
|
|
45
|
+
else
|
|
46
|
+
hard_stop "Source branch '$SOURCE_BRANCH' not found locally or on remote."
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Cleanup helper — used on failure to restore state
|
|
51
|
+
on_fail() {
|
|
52
|
+
git merge --abort 2>/dev/null || true
|
|
53
|
+
git checkout "$ORIGINAL_BRANCH" 2>/dev/null \
|
|
54
|
+
|| git checkout "$DEFAULT_BRANCH" 2>/dev/null || true
|
|
55
|
+
git branch -D "$TEST_BRANCH" 2>/dev/null || true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# 1. Sync local default with remote
|
|
59
|
+
info "Syncing local $DEFAULT_BRANCH with origin..."
|
|
60
|
+
git fetch origin "$DEFAULT_BRANCH"
|
|
61
|
+
git checkout "$DEFAULT_BRANCH"
|
|
62
|
+
if ! git pull --ff-only origin "$DEFAULT_BRANCH"; then
|
|
63
|
+
on_fail
|
|
64
|
+
hard_stop "Cannot fast-forward local $DEFAULT_BRANCH from remote — resolve manually."
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# 2. Create ephemeral test branch from current default tip
|
|
68
|
+
if git rev-parse --verify "$TEST_BRANCH" &>/dev/null; then
|
|
69
|
+
warn "Stale test branch '$TEST_BRANCH' exists from a previous run — deleting."
|
|
70
|
+
git branch -D "$TEST_BRANCH"
|
|
71
|
+
fi
|
|
72
|
+
git checkout -b "$TEST_BRANCH"
|
|
73
|
+
|
|
74
|
+
# 3. Merge source into test branch (local only)
|
|
75
|
+
info "Test-merging '$SOURCE_BRANCH' into '$TEST_BRANCH'..."
|
|
76
|
+
if ! git merge --no-ff -m "test-merge: $SOURCE_BRANCH" "$SOURCE_BRANCH"; then
|
|
77
|
+
on_fail
|
|
78
|
+
hard_stop "Merge conflict: '$SOURCE_BRANCH' cannot merge cleanly into '$DEFAULT_BRANCH'. Resolve before retrying."
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# 4. Run validators against merged tree
|
|
82
|
+
echo ""
|
|
83
|
+
info "Running validators against merged tree..."
|
|
84
|
+
echo ""
|
|
85
|
+
if ! python3 "$VALIDATOR" "$REPO_ROOT"; then
|
|
86
|
+
echo ""
|
|
87
|
+
on_fail
|
|
88
|
+
hard_stop "Test-merge gate FAILED for '$SOURCE_BRANCH'. Local '$DEFAULT_BRANCH' is unchanged."
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# 5. Pass: fast-forward default to test-merge tip; clean up test branch
|
|
92
|
+
echo ""
|
|
93
|
+
info "Test-merge gate PASSED. Promoting to local '$DEFAULT_BRANCH'..."
|
|
94
|
+
git checkout "$DEFAULT_BRANCH"
|
|
95
|
+
git merge --ff-only "$TEST_BRANCH"
|
|
96
|
+
git branch -d "$TEST_BRANCH"
|
|
97
|
+
|
|
98
|
+
echo ""
|
|
99
|
+
info "✓ Local '$DEFAULT_BRANCH' now contains the merge of '$SOURCE_BRANCH'."
|
|
100
|
+
info " Caller should: git push origin $DEFAULT_BRANCH"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Knowledge Organization Standard enforcement (POL-416).
|
|
4
|
+
|
|
5
|
+
Checks every *.md under knowledge/ (org tree only — framework/ is the
|
|
6
|
+
upstream template and excluded):
|
|
7
|
+
|
|
8
|
+
1. front-matter — present, schema-valid, domain/layer agree with the
|
|
9
|
+
file's folder (domain-root instruments are exempt from layer-folder
|
|
10
|
+
agreement; their paths are pinned by policy text — see the C03
|
|
11
|
+
deviation in the PRJ-005 migration).
|
|
12
|
+
2. orphan check — every non-README doc is linked from at least one other
|
|
13
|
+
knowledge doc (its layer index or a journey doc).
|
|
14
|
+
3. journey purity — paths/*.md are links-in-order docs: no code blocks,
|
|
15
|
+
no embedded images, and a minimum link density.
|
|
16
|
+
4. link check — every relative md link resolves; no [[wikilinks]].
|
|
17
|
+
5. diagram rule — no binary diagram embeds (png/jpg/gif) in knowledge/;
|
|
18
|
+
diagrams are Mermaid text (POL-414). Files whose link path contains
|
|
19
|
+
"screenshot" are exempt.
|
|
20
|
+
|
|
21
|
+
Superseded redirect stubs (status: superseded) are exempt from orphan and
|
|
22
|
+
folder-agreement checks but must still parse.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
DOMAINS = {
|
|
29
|
+
"policies", "legal", "architecture/system", "architecture/data",
|
|
30
|
+
"development", "testing", "deployment", "infrastructure", "support",
|
|
31
|
+
"compliance", "navigation",
|
|
32
|
+
}
|
|
33
|
+
LAYERS = {"mandate", "procedure", "pattern", "use-case", "spec", "compliance", "path"}
|
|
34
|
+
COMPLIANCE = {"C01", "C02", "C03", "instructional", "descriptive", "evidence"}
|
|
35
|
+
STATUSES = {"current", "draft", "superseded"}
|
|
36
|
+
LAYER_FOLDER = {
|
|
37
|
+
"mandates": "mandate", "procedures": "procedure", "patterns": "pattern",
|
|
38
|
+
"use-cases": "use-case", "specs": "spec", "compliance": "compliance",
|
|
39
|
+
"paths": "path",
|
|
40
|
+
}
|
|
41
|
+
FM_RE = re.compile(r"\A---\n(.*?)\n---\n", re.S)
|
|
42
|
+
LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
|
|
43
|
+
WIKILINK_RE = re.compile(r"\[\[[^\]]+\]\]")
|
|
44
|
+
IMG_RE = re.compile(r"!\[[^\]]*\]\(([^)\s]+)\)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _front_matter(text: str) -> dict | None:
|
|
48
|
+
m = FM_RE.match(text)
|
|
49
|
+
if not m:
|
|
50
|
+
return None
|
|
51
|
+
fm = {}
|
|
52
|
+
for line in m.group(1).splitlines():
|
|
53
|
+
if ":" in line and not line.lstrip().startswith("#"):
|
|
54
|
+
k, _, v = line.partition(":")
|
|
55
|
+
fm[k.strip()] = v.strip()
|
|
56
|
+
return fm
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def check_knowledge(repo_root: Path) -> list[str]:
|
|
60
|
+
errors: list[str] = []
|
|
61
|
+
kroot = repo_root / "knowledge"
|
|
62
|
+
if not kroot.is_dir():
|
|
63
|
+
return ["knowledge/ directory missing"]
|
|
64
|
+
|
|
65
|
+
docs: dict[Path, str] = {
|
|
66
|
+
p: p.read_text(encoding="utf-8", errors="replace")
|
|
67
|
+
for p in sorted(kroot.rglob("*.md"))
|
|
68
|
+
}
|
|
69
|
+
linked: set[Path] = set()
|
|
70
|
+
|
|
71
|
+
def _strip_code(t: str) -> str:
|
|
72
|
+
# CommonMark-ish fence matching: a fence opened with N backticks only
|
|
73
|
+
# closes on a line with >= N backticks (so ````markdown wrappers can
|
|
74
|
+
# embed ``` blocks). Then inline spans; then indented code (4+ spaces).
|
|
75
|
+
out: list[str] = []
|
|
76
|
+
fence_len = 0 # 0 = not in a fence
|
|
77
|
+
for l in t.splitlines():
|
|
78
|
+
stripped = l.lstrip()
|
|
79
|
+
m = re.match(r"^(`{3,})", stripped)
|
|
80
|
+
if m:
|
|
81
|
+
n = len(m.group(1))
|
|
82
|
+
if fence_len == 0:
|
|
83
|
+
fence_len = n # open
|
|
84
|
+
elif n >= fence_len:
|
|
85
|
+
fence_len = 0 # close
|
|
86
|
+
continue
|
|
87
|
+
if fence_len == 0:
|
|
88
|
+
out.append(l)
|
|
89
|
+
t = re.sub(r"`[^`\n]*`", "", "\n".join(out))
|
|
90
|
+
return "\n".join(l for l in t.splitlines() if not l.startswith(" "))
|
|
91
|
+
|
|
92
|
+
# Pass 1 — links, wikilinks, images, and link-graph construction
|
|
93
|
+
for p, text in docs.items():
|
|
94
|
+
rel = p.relative_to(repo_root)
|
|
95
|
+
if WIKILINK_RE.search(_strip_code(text)):
|
|
96
|
+
errors.append(f"{rel}: [[wikilink]] found — use relative markdown links (POL-413)")
|
|
97
|
+
for target in IMG_RE.findall(text):
|
|
98
|
+
if target.lower().endswith((".png", ".jpg", ".jpeg", ".gif")) and "screenshot" not in target.lower():
|
|
99
|
+
errors.append(f"{rel}: binary diagram embed '{target}' — diagrams are Mermaid text (POL-414)")
|
|
100
|
+
for target in LINK_RE.findall(text):
|
|
101
|
+
if target.startswith(("http://", "https://", "mailto:", "#")):
|
|
102
|
+
continue
|
|
103
|
+
tpath = (p.parent / target.split("#")[0]).resolve()
|
|
104
|
+
if not tpath.exists():
|
|
105
|
+
errors.append(f"{rel}: broken link '{target}'")
|
|
106
|
+
else:
|
|
107
|
+
try:
|
|
108
|
+
linked.add(tpath.relative_to(repo_root.resolve()))
|
|
109
|
+
except ValueError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# Pass 2 — front-matter, folder agreement, orphan, journey purity
|
|
113
|
+
for p, text in docs.items():
|
|
114
|
+
rel = p.relative_to(repo_root)
|
|
115
|
+
fm = _front_matter(text)
|
|
116
|
+
if fm is None:
|
|
117
|
+
errors.append(f"{rel}: missing front-matter (POL-408)")
|
|
118
|
+
continue
|
|
119
|
+
for key, allowed in (("domain", DOMAINS), ("layer", LAYERS),
|
|
120
|
+
("compliance", COMPLIANCE), ("status", STATUSES)):
|
|
121
|
+
val = fm.get(key)
|
|
122
|
+
if val not in allowed:
|
|
123
|
+
errors.append(f"{rel}: front-matter {key}='{val}' invalid (POL-408)")
|
|
124
|
+
if not fm.get("owner"):
|
|
125
|
+
errors.append(f"{rel}: front-matter owner missing (POL-408)")
|
|
126
|
+
|
|
127
|
+
if fm.get("status") == "superseded":
|
|
128
|
+
continue # redirect stubs: parse-only
|
|
129
|
+
|
|
130
|
+
parts = rel.parts # ('knowledge', <domain..>, [layer], file)
|
|
131
|
+
folder_layer = None
|
|
132
|
+
for seg in parts[1:-1]:
|
|
133
|
+
if seg in LAYER_FOLDER:
|
|
134
|
+
folder_layer = LAYER_FOLDER[seg]
|
|
135
|
+
if folder_layer and fm.get("layer") != folder_layer:
|
|
136
|
+
errors.append(
|
|
137
|
+
f"{rel}: layer '{fm.get('layer')}' disagrees with folder '{folder_layer}' (POL-408)"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Orphan check: non-README docs must be linked from somewhere.
|
|
141
|
+
if p.name != "README.md":
|
|
142
|
+
try:
|
|
143
|
+
relresolved = p.resolve().relative_to(repo_root.resolve())
|
|
144
|
+
except ValueError:
|
|
145
|
+
relresolved = rel
|
|
146
|
+
if relresolved not in linked:
|
|
147
|
+
errors.append(f"{rel}: orphan — not linked from any index or journey (POL-416)")
|
|
148
|
+
|
|
149
|
+
# Journey purity
|
|
150
|
+
if parts[1] == "paths" and p.name != "README.md":
|
|
151
|
+
if "```" in text:
|
|
152
|
+
errors.append(f"{rel}: journey docs are links-only — code block found (POL-410)")
|
|
153
|
+
if IMG_RE.search(text):
|
|
154
|
+
errors.append(f"{rel}: journey docs are links-only — image found (POL-410)")
|
|
155
|
+
if len(LINK_RE.findall(text)) < 3:
|
|
156
|
+
errors.append(f"{rel}: journey doc has fewer than 3 links — is it a journey? (POL-410)")
|
|
157
|
+
|
|
158
|
+
return errors
|