forgedock 1.0.22 → 1.1.2
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/README.md +255 -211
- package/bin/batch-runner.mjs +339 -0
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +168 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +32 -0
- package/bin/engine-cli.mjs +222 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +896 -1540
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +841 -0
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +136 -20
- package/bin/settings-hook.mjs +346 -0
- package/bin/tui.mjs +6 -3
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +84 -12
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +63 -4
- package/commands/design-bench.md +6 -1
- package/commands/design-render-critique-loop.md +2 -1
- package/commands/design.md +1 -0
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +122 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +24 -3
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2027
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +326 -42
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +612 -56
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +19 -5
- package/commands/work-on/build/validate.md +62 -6
- package/commands/work-on/build.md +187 -30
- package/commands/work-on/close.md +132 -5
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +48 -4
- package/commands/work-on-monolithic.md +8 -2
- package/commands/work-on.md +247 -7
- package/docs/BRAND.md +16 -5
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +44 -42
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +117 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +339 -113
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +58 -12
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +10 -2
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +12 -4
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +229 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- package/templates/devdocs/agent/using-forgedock.md +14 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# code-index.sh — Per-commit symbol index and import graph for ForgeDock agents
|
|
3
|
+
#
|
|
4
|
+
# Builds a deterministic code map (symbols + import graph + file-to-domain mapping)
|
|
5
|
+
# keyed by HEAD SHA. Cached under .forge/index/ to avoid redundant rebuilds.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# scripts/code-index.sh [--repo-path <path>] [--output <dir>] [--force]
|
|
9
|
+
# scripts/code-index.sh query --symbol <name> [--repo-path <path>]
|
|
10
|
+
# scripts/code-index.sh query --callers <name> [--repo-path <path>]
|
|
11
|
+
# scripts/code-index.sh query --importers <file> [--repo-path <path>]
|
|
12
|
+
# scripts/code-index.sh query --domain <label> [--repo-path <path>]
|
|
13
|
+
#
|
|
14
|
+
# Output files (under CACHE_DIR/{HEAD_SHA}/):
|
|
15
|
+
# symbols.tsv — symbol_name TAB file TAB line TAB kind (function|class|method|const)
|
|
16
|
+
# imports.tsv — importer_file TAB imported_module TAB imported_symbol
|
|
17
|
+
# file_domain.tsv — file_path TAB domain_label
|
|
18
|
+
# callers.tsv — callee_symbol TAB caller_file TAB caller_line
|
|
19
|
+
# index.json — machine-readable manifest with metadata
|
|
20
|
+
#
|
|
21
|
+
# Tooling:
|
|
22
|
+
# Primary: ctags (universal-ctags preferred) for symbol extraction
|
|
23
|
+
# Fallback: grep-based extraction when ctags is unavailable
|
|
24
|
+
# Import graph: language-specific grep patterns (Python, JS/TS, Go, Rust, shell)
|
|
25
|
+
#
|
|
26
|
+
# Cache key: HEAD SHA — cache is a hit on unchanged HEAD, miss on new commit.
|
|
27
|
+
# Works on Windows (Git Bash / WSL) and Linux. No bash-only hot-path tooling.
|
|
28
|
+
#
|
|
29
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
30
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
31
|
+
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Argument parsing
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
QUERY_MODE=""
|
|
38
|
+
QUERY_TYPE=""
|
|
39
|
+
QUERY_ARG=""
|
|
40
|
+
REPO_PATH="${PWD}"
|
|
41
|
+
OUTPUT_DIR=""
|
|
42
|
+
FORCE=0
|
|
43
|
+
|
|
44
|
+
while [[ $# -gt 0 ]]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
query)
|
|
47
|
+
QUERY_MODE=1
|
|
48
|
+
shift
|
|
49
|
+
;;
|
|
50
|
+
--symbol)
|
|
51
|
+
QUERY_TYPE="symbol"
|
|
52
|
+
QUERY_ARG="$2"
|
|
53
|
+
shift 2
|
|
54
|
+
;;
|
|
55
|
+
--callers)
|
|
56
|
+
QUERY_TYPE="callers"
|
|
57
|
+
QUERY_ARG="$2"
|
|
58
|
+
shift 2
|
|
59
|
+
;;
|
|
60
|
+
--importers)
|
|
61
|
+
QUERY_TYPE="importers"
|
|
62
|
+
QUERY_ARG="$2"
|
|
63
|
+
shift 2
|
|
64
|
+
;;
|
|
65
|
+
--domain)
|
|
66
|
+
QUERY_TYPE="domain"
|
|
67
|
+
QUERY_ARG="$2"
|
|
68
|
+
shift 2
|
|
69
|
+
;;
|
|
70
|
+
--repo-path)
|
|
71
|
+
REPO_PATH="$2"
|
|
72
|
+
shift 2
|
|
73
|
+
;;
|
|
74
|
+
--output)
|
|
75
|
+
OUTPUT_DIR="$2"
|
|
76
|
+
shift 2
|
|
77
|
+
;;
|
|
78
|
+
--force)
|
|
79
|
+
FORCE=1
|
|
80
|
+
shift
|
|
81
|
+
;;
|
|
82
|
+
*)
|
|
83
|
+
echo "Unknown argument: $1" >&2
|
|
84
|
+
exit 1
|
|
85
|
+
;;
|
|
86
|
+
esac
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Resolve paths
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
REPO_PATH=$(cd "$REPO_PATH" 2>/dev/null && pwd || echo "$REPO_PATH")
|
|
93
|
+
CACHE_BASE="${OUTPUT_DIR:-${REPO_PATH}/.forge/index}"
|
|
94
|
+
|
|
95
|
+
# Read HEAD SHA (portable: works when git is available, otherwise use timestamp)
|
|
96
|
+
if command -v git >/dev/null 2>&1 && git -C "$REPO_PATH" rev-parse HEAD >/dev/null 2>&1; then
|
|
97
|
+
HEAD_SHA=$(git -C "$REPO_PATH" rev-parse HEAD)
|
|
98
|
+
else
|
|
99
|
+
HEAD_SHA="no-git-$(date +%s)"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
CACHE_DIR="${CACHE_BASE}/${HEAD_SHA}"
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Query mode — serve cached data without rebuilding
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
if [[ -n "$QUERY_MODE" ]]; then
|
|
108
|
+
if [[ ! -d "$CACHE_DIR" ]]; then
|
|
109
|
+
echo "ERROR: No index found for HEAD ${HEAD_SHA:0:8}. Run code-index.sh to build first." >&2
|
|
110
|
+
echo "Hint: cd \"${REPO_PATH}\" && scripts/code-index.sh --repo-path \"${REPO_PATH}\"" >&2
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
case "$QUERY_TYPE" in
|
|
115
|
+
symbol)
|
|
116
|
+
if [[ -f "${CACHE_DIR}/symbols.tsv" ]]; then
|
|
117
|
+
grep -i "^${QUERY_ARG}"$'\t' "${CACHE_DIR}/symbols.tsv" || true
|
|
118
|
+
fi
|
|
119
|
+
;;
|
|
120
|
+
callers)
|
|
121
|
+
if [[ -f "${CACHE_DIR}/callers.tsv" ]]; then
|
|
122
|
+
grep -i "^${QUERY_ARG}"$'\t' "${CACHE_DIR}/callers.tsv" || true
|
|
123
|
+
fi
|
|
124
|
+
;;
|
|
125
|
+
importers)
|
|
126
|
+
if [[ -f "${CACHE_DIR}/imports.tsv" ]]; then
|
|
127
|
+
grep -i $'\t'"${QUERY_ARG}"$'\t' "${CACHE_DIR}/imports.tsv" || \
|
|
128
|
+
grep -i $'\t'"${QUERY_ARG}"$ "${CACHE_DIR}/imports.tsv" || true
|
|
129
|
+
fi
|
|
130
|
+
;;
|
|
131
|
+
domain)
|
|
132
|
+
if [[ -f "${CACHE_DIR}/file_domain.tsv" ]]; then
|
|
133
|
+
grep -i $'\t'"${QUERY_ARG}"$ "${CACHE_DIR}/file_domain.tsv" || true
|
|
134
|
+
fi
|
|
135
|
+
;;
|
|
136
|
+
*)
|
|
137
|
+
echo "ERROR: Unknown query type. Use --symbol, --callers, --importers, or --domain" >&2
|
|
138
|
+
exit 1
|
|
139
|
+
;;
|
|
140
|
+
esac
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Build mode — check cache hit first
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
if [[ -d "$CACHE_DIR" && -f "${CACHE_DIR}/index.json" && "$FORCE" -eq 0 ]]; then
|
|
148
|
+
echo "INFO: Cache hit for HEAD ${HEAD_SHA:0:8} — skipping rebuild"
|
|
149
|
+
echo "INFO: Index at ${CACHE_DIR}"
|
|
150
|
+
cat "${CACHE_DIR}/index.json"
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
echo "INFO: Building code index for HEAD ${HEAD_SHA:0:8}..."
|
|
155
|
+
mkdir -p "$CACHE_DIR"
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Step 1: Symbol extraction
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
SYMBOLS_FILE="${CACHE_DIR}/symbols.tsv"
|
|
161
|
+
> "$SYMBOLS_FILE"
|
|
162
|
+
|
|
163
|
+
echo "INFO: Extracting symbols..."
|
|
164
|
+
|
|
165
|
+
if command -v ctags >/dev/null 2>&1; then
|
|
166
|
+
# Use ctags for precise symbol extraction
|
|
167
|
+
# --fields=+ne adds line number and end-of-scope — portable across universal-ctags
|
|
168
|
+
ctags -R --output-format=json \
|
|
169
|
+
--fields='nkz' \
|
|
170
|
+
--languages=Python,JavaScript,TypeScript,Go,Rust,Java,C,C++ \
|
|
171
|
+
-f - "$REPO_PATH" 2>/dev/null \
|
|
172
|
+
| grep -v '^!' \
|
|
173
|
+
| while IFS= read -r line; do
|
|
174
|
+
# Parse JSON output from universal-ctags. Portable (non-PCRE) extraction —
|
|
175
|
+
# grep -oP is not supported by Git Bash's grep build on Windows, so use a
|
|
176
|
+
# bash regex instead of a lookbehind.
|
|
177
|
+
if [[ "$line" =~ \"name\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
|
|
178
|
+
name="${BASH_REMATCH[1]}"
|
|
179
|
+
else
|
|
180
|
+
name=""
|
|
181
|
+
fi
|
|
182
|
+
if [[ "$line" =~ \"path\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
|
|
183
|
+
path="${BASH_REMATCH[1]}"
|
|
184
|
+
else
|
|
185
|
+
path=""
|
|
186
|
+
fi
|
|
187
|
+
if [[ "$line" =~ \"line\"[[:space:]]*:[[:space:]]*([0-9]+) ]]; then
|
|
188
|
+
line_num="${BASH_REMATCH[1]}"
|
|
189
|
+
else
|
|
190
|
+
line_num="0"
|
|
191
|
+
fi
|
|
192
|
+
if [[ "$line" =~ \"kind\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
|
|
193
|
+
kind="${BASH_REMATCH[1]}"
|
|
194
|
+
else
|
|
195
|
+
kind="unknown"
|
|
196
|
+
fi
|
|
197
|
+
if [[ -n "$name" && -n "$path" ]]; then
|
|
198
|
+
echo -e "${name}\t${path}\t${line_num}\t${kind}"
|
|
199
|
+
fi
|
|
200
|
+
done >> "$SYMBOLS_FILE" 2>/dev/null || true
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# Fallback: grep-based extraction for Python functions/classes
|
|
204
|
+
if [[ ! -s "$SYMBOLS_FILE" ]] || ! command -v ctags >/dev/null 2>&1; then
|
|
205
|
+
echo "INFO: Falling back to grep-based symbol extraction"
|
|
206
|
+
|
|
207
|
+
# Python: functions and classes
|
|
208
|
+
find "$REPO_PATH" -name "*.py" -not -path "*/.git/*" -not -path "*/node_modules/*" \
|
|
209
|
+
-not -path "*/__pycache__/*" 2>/dev/null | while read -r f; do
|
|
210
|
+
grep -n "^def \|^ def \|^class " "$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
211
|
+
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
212
|
+
# grep build on Windows, so use a bash regex instead of a lookbehind.
|
|
213
|
+
if [[ "$content" =~ (def|class)[[:space:]]+([A-Za-z_][A-Za-z0-9_]*) ]]; then
|
|
214
|
+
name="${BASH_REMATCH[2]}"
|
|
215
|
+
else
|
|
216
|
+
name=""
|
|
217
|
+
fi
|
|
218
|
+
kind=$(echo "$content" | grep -q "^def \| def " && echo "function" || echo "class")
|
|
219
|
+
rel_path="${f#$REPO_PATH/}"
|
|
220
|
+
[[ -n "$name" ]] && echo -e "${name}\t${rel_path}\t${lnum}\t${kind}"
|
|
221
|
+
done || true
|
|
222
|
+
done >> "$SYMBOLS_FILE" 2>/dev/null || true
|
|
223
|
+
|
|
224
|
+
# JavaScript/TypeScript: functions and classes
|
|
225
|
+
find "$REPO_PATH" \( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.jsx" \) \
|
|
226
|
+
-not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null | while read -r f; do
|
|
227
|
+
grep -n "^export function \|^function \|^export class \|^class \|^const .* = \(function\|async function\|() =>\)" \
|
|
228
|
+
"$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
229
|
+
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
230
|
+
# grep build on Windows, so use a bash regex instead of lookbehind/lookahead.
|
|
231
|
+
if [[ "$content" =~ (function|class)[[:space:]]+([A-Za-z_][A-Za-z0-9_]*) ]]; then
|
|
232
|
+
name="${BASH_REMATCH[2]}"
|
|
233
|
+
elif [[ "$content" =~ const[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*= ]]; then
|
|
234
|
+
name="${BASH_REMATCH[1]}"
|
|
235
|
+
else
|
|
236
|
+
name=""
|
|
237
|
+
fi
|
|
238
|
+
rel_path="${f#$REPO_PATH/}"
|
|
239
|
+
[[ -n "$name" ]] && echo -e "${name}\t${rel_path}\t${lnum}\tfunction"
|
|
240
|
+
done || true
|
|
241
|
+
done >> "$SYMBOLS_FILE" 2>/dev/null || true
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Step 2: Import graph extraction
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
IMPORTS_FILE="${CACHE_DIR}/imports.tsv"
|
|
248
|
+
> "$IMPORTS_FILE"
|
|
249
|
+
|
|
250
|
+
echo "INFO: Building import graph..."
|
|
251
|
+
|
|
252
|
+
# Python imports
|
|
253
|
+
find "$REPO_PATH" -name "*.py" -not -path "*/.git/*" -not -path "*/__pycache__/*" \
|
|
254
|
+
2>/dev/null | while read -r f; do
|
|
255
|
+
rel_path="${f#$REPO_PATH/}"
|
|
256
|
+
grep -n "^import \|^from " "$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
257
|
+
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
258
|
+
# grep build on Windows, so use a bash regex instead of lookbehind.
|
|
259
|
+
if [[ "$content" =~ ^from[[:space:]]+([^[:space:]]+)[[:space:]]+import[[:space:]]+(.+)$ ]]; then
|
|
260
|
+
module="${BASH_REMATCH[1]}"
|
|
261
|
+
symbol="${BASH_REMATCH[2]}"
|
|
262
|
+
elif [[ "$content" =~ ^import[[:space:]]+([^[:space:]]+) ]]; then
|
|
263
|
+
module="${BASH_REMATCH[1]}"
|
|
264
|
+
symbol=""
|
|
265
|
+
else
|
|
266
|
+
module=""
|
|
267
|
+
symbol=""
|
|
268
|
+
fi
|
|
269
|
+
[[ -n "$module" ]] && echo -e "${rel_path}\t${module}\t${symbol}"
|
|
270
|
+
done || true
|
|
271
|
+
done >> "$IMPORTS_FILE" 2>/dev/null || true
|
|
272
|
+
|
|
273
|
+
# JavaScript/TypeScript imports
|
|
274
|
+
find "$REPO_PATH" \( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.jsx" \) \
|
|
275
|
+
-not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null | while read -r f; do
|
|
276
|
+
rel_path="${f#$REPO_PATH/}"
|
|
277
|
+
grep -n "^import \|^} from \|require(" "$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
278
|
+
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
279
|
+
# grep build on Windows, so use a bash regex instead of lookbehind/lookahead.
|
|
280
|
+
if [[ "$content" =~ from[[:space:]]*[\'\"]([^\'\"]+)[\'\"] ]]; then
|
|
281
|
+
module="${BASH_REMATCH[1]}"
|
|
282
|
+
elif [[ "$content" =~ require\([[:space:]]*[\'\"]([^\'\"]+)[\'\"] ]]; then
|
|
283
|
+
module="${BASH_REMATCH[1]}"
|
|
284
|
+
else
|
|
285
|
+
module=""
|
|
286
|
+
fi
|
|
287
|
+
[[ -n "$module" ]] && echo -e "${rel_path}\t${module}\t"
|
|
288
|
+
done || true
|
|
289
|
+
done >> "$IMPORTS_FILE" 2>/dev/null || true
|
|
290
|
+
|
|
291
|
+
# Go imports
|
|
292
|
+
find "$REPO_PATH" -name "*.go" -not -path "*/.git/*" 2>/dev/null | while read -r f; do
|
|
293
|
+
rel_path="${f#$REPO_PATH/}"
|
|
294
|
+
grep -n '"[^"]*"' "$f" 2>/dev/null | grep -v "//\|fmt.Print\|errors.New" | while IFS=: read -r lnum content; do
|
|
295
|
+
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
296
|
+
# grep build on Windows, so use a bash regex instead of lookbehind/lookahead.
|
|
297
|
+
if [[ "$content" =~ \"([^\"]+)\" ]]; then
|
|
298
|
+
module="${BASH_REMATCH[1]}"
|
|
299
|
+
else
|
|
300
|
+
module=""
|
|
301
|
+
fi
|
|
302
|
+
[[ -n "$module" ]] && echo -e "${rel_path}\t${module}\t"
|
|
303
|
+
done || true
|
|
304
|
+
done >> "$IMPORTS_FILE" 2>/dev/null || true
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Step 3: Caller graph (callee → callers mapping)
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
CALLERS_FILE="${CACHE_DIR}/callers.tsv"
|
|
310
|
+
> "$CALLERS_FILE"
|
|
311
|
+
|
|
312
|
+
echo "INFO: Building caller graph..."
|
|
313
|
+
|
|
314
|
+
# For each symbol, find files that call it (grep for usage)
|
|
315
|
+
if [[ -s "$SYMBOLS_FILE" ]]; then
|
|
316
|
+
# Process a sample of top symbols (cap at 500 to avoid timeout)
|
|
317
|
+
head -500 "$SYMBOLS_FILE" | while IFS=$'\t' read -r sym_name sym_file sym_line sym_kind; do
|
|
318
|
+
# Skip very short or generic names to avoid false positives
|
|
319
|
+
if [[ ${#sym_name} -lt 3 ]]; then continue; fi
|
|
320
|
+
# Search for callers (function calls, not definitions)
|
|
321
|
+
find "$REPO_PATH" -type f \( -name "*.py" -o -name "*.js" -o -name "*.ts" \
|
|
322
|
+
-o -name "*.go" -o -name "*.rs" -o -name "*.java" \) \
|
|
323
|
+
-not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null | \
|
|
324
|
+
xargs grep -l "\b${sym_name}(" 2>/dev/null | while read -r caller_file; do
|
|
325
|
+
rel_caller="${caller_file#$REPO_PATH/}"
|
|
326
|
+
# Skip the file where the symbol is defined
|
|
327
|
+
if [[ "$rel_caller" == "$sym_file" ]]; then continue; fi
|
|
328
|
+
caller_line=$(grep -n "\b${sym_name}(" "$caller_file" 2>/dev/null | head -1 | cut -d: -f1 || echo "0")
|
|
329
|
+
echo -e "${sym_name}\t${rel_caller}\t${caller_line}"
|
|
330
|
+
done || true
|
|
331
|
+
# ^ `xargs grep -l` returning 1 (zero callers found for this symbol) propagates
|
|
332
|
+
# through `pipefail` into this `while` compound statement, which is subject to
|
|
333
|
+
# `set -e` — guard it here so one symbol with no callers doesn't abort the
|
|
334
|
+
# remaining symbols in the outer loop.
|
|
335
|
+
done >> "$CALLERS_FILE" 2>/dev/null || true
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# Step 4: File-to-domain mapping
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
DOMAIN_FILE="${CACHE_DIR}/file_domain.tsv"
|
|
342
|
+
> "$DOMAIN_FILE"
|
|
343
|
+
|
|
344
|
+
echo "INFO: Building file-domain map..."
|
|
345
|
+
|
|
346
|
+
# Read forge.yaml domain hints if available
|
|
347
|
+
FORGE_YAML="${REPO_PATH}/forge.yaml"
|
|
348
|
+
declare -A DOMAIN_PATTERNS
|
|
349
|
+
DOMAIN_PATTERNS=(
|
|
350
|
+
["commands"]="pipeline-commands"
|
|
351
|
+
["scripts"]="pipeline-scripts"
|
|
352
|
+
["services/api"]="api"
|
|
353
|
+
["services/worker"]="worker"
|
|
354
|
+
["web/src/app/billing"]="billing"
|
|
355
|
+
["web/src/app/auth"]="auth"
|
|
356
|
+
["services/api/auth"]="auth"
|
|
357
|
+
["services/api/billing"]="billing"
|
|
358
|
+
["services/api/db"]="database"
|
|
359
|
+
["migrations"]="database"
|
|
360
|
+
[".github/workflows"]="ci-cd"
|
|
361
|
+
["infra"]="infrastructure"
|
|
362
|
+
["web/src"]="frontend"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Read key_paths from forge.yaml if present (override defaults)
|
|
366
|
+
if [[ -f "$FORGE_YAML" ]]; then
|
|
367
|
+
# Simple key_paths extraction — reads "domain: [patterns]" blocks under review.key_paths
|
|
368
|
+
while IFS=: read -r key val; do
|
|
369
|
+
key=$(echo "$key" | tr -d ' ')
|
|
370
|
+
val=$(echo "$val" | tr -d ' "')
|
|
371
|
+
if [[ -n "$key" && -n "$val" ]]; then
|
|
372
|
+
DOMAIN_PATTERNS["$val"]="$key"
|
|
373
|
+
fi
|
|
374
|
+
done < <(grep -A2 'key_paths:' "$FORGE_YAML" 2>/dev/null | grep '^\s*\w\+:' | head -20 || true)
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
# Map each file to its domain
|
|
378
|
+
find "$REPO_PATH" -type f -not -path "*/.git/*" -not -path "*/node_modules/*" \
|
|
379
|
+
-not -path "*/__pycache__/*" -not -path "*/.forge/*" 2>/dev/null | while read -r f; do
|
|
380
|
+
rel_path="${f#$REPO_PATH/}"
|
|
381
|
+
domain="unclassified"
|
|
382
|
+
for pattern in "${!DOMAIN_PATTERNS[@]}"; do
|
|
383
|
+
if [[ "$rel_path" == $pattern* ]]; then
|
|
384
|
+
domain="${DOMAIN_PATTERNS[$pattern]}"
|
|
385
|
+
break
|
|
386
|
+
fi
|
|
387
|
+
done
|
|
388
|
+
echo -e "${rel_path}\t${domain}"
|
|
389
|
+
done >> "$DOMAIN_FILE" 2>/dev/null || true
|
|
390
|
+
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
# Step 5: Write index manifest
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
|
|
395
|
+
SYMBOL_COUNT=$(wc -l < "$SYMBOLS_FILE" 2>/dev/null || echo 0)
|
|
396
|
+
IMPORT_COUNT=$(wc -l < "$IMPORTS_FILE" 2>/dev/null || echo 0)
|
|
397
|
+
CALLER_COUNT=$(wc -l < "$CALLERS_FILE" 2>/dev/null || echo 0)
|
|
398
|
+
FILE_COUNT=$(wc -l < "$DOMAIN_FILE" 2>/dev/null || echo 0)
|
|
399
|
+
|
|
400
|
+
# Sanity check: if source files that should have produced symbols/imports were
|
|
401
|
+
# found, but extraction came back empty, a loop was truncated somewhere above
|
|
402
|
+
# (or a tool like grep -oP is unavailable/incompatible on this platform) — flag
|
|
403
|
+
# it instead of letting "Index built successfully" silently paper over data loss.
|
|
404
|
+
SRC_FILE_COUNT=$(find "$REPO_PATH" \( -name "*.py" -o -name "*.js" -o -name "*.ts" \
|
|
405
|
+
-o -name "*.tsx" -o -name "*.jsx" -o -name "*.go" \) -not -path "*/.git/*" \
|
|
406
|
+
-not -path "*/node_modules/*" -not -path "*/__pycache__/*" 2>/dev/null | wc -l | tr -d ' ')
|
|
407
|
+
INDEX_TRUNCATED=0
|
|
408
|
+
if [[ "$SRC_FILE_COUNT" -gt 0 && "$SYMBOL_COUNT" -eq 0 ]]; then
|
|
409
|
+
INDEX_TRUNCATED=1
|
|
410
|
+
echo "WARN: 0 symbols extracted despite ${SRC_FILE_COUNT} matching source file(s) found." >&2
|
|
411
|
+
echo "WARN: Extraction may have been truncated by a shell/grep failure, or ctags/grep tooling is unavailable/incompatible on this platform." >&2
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
cat > "${CACHE_DIR}/index.json" << INDEX_EOF
|
|
415
|
+
{
|
|
416
|
+
"schema_version": "1",
|
|
417
|
+
"head_sha": "${HEAD_SHA}",
|
|
418
|
+
"built_at": "${TIMESTAMP}",
|
|
419
|
+
"repo_path": "${REPO_PATH}",
|
|
420
|
+
"cache_dir": "${CACHE_DIR}",
|
|
421
|
+
"stats": {
|
|
422
|
+
"symbols": ${SYMBOL_COUNT},
|
|
423
|
+
"imports": ${IMPORT_COUNT},
|
|
424
|
+
"callers": ${CALLER_COUNT},
|
|
425
|
+
"files": ${FILE_COUNT},
|
|
426
|
+
"truncated": $([[ "$INDEX_TRUNCATED" -eq 1 ]] && echo "true" || echo "false")
|
|
427
|
+
},
|
|
428
|
+
"files": {
|
|
429
|
+
"symbols": "${CACHE_DIR}/symbols.tsv",
|
|
430
|
+
"imports": "${CACHE_DIR}/imports.tsv",
|
|
431
|
+
"callers": "${CACHE_DIR}/callers.tsv",
|
|
432
|
+
"file_domain": "${CACHE_DIR}/file_domain.tsv"
|
|
433
|
+
},
|
|
434
|
+
"query_usage": {
|
|
435
|
+
"symbol_lookup": "scripts/code-index.sh query --symbol <name> --repo-path <path>",
|
|
436
|
+
"caller_lookup": "scripts/code-index.sh query --callers <name> --repo-path <path>",
|
|
437
|
+
"importer_lookup": "scripts/code-index.sh query --importers <file> --repo-path <path>",
|
|
438
|
+
"domain_lookup": "scripts/code-index.sh query --domain <label> --repo-path <path>"
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
INDEX_EOF
|
|
442
|
+
|
|
443
|
+
if [[ "$INDEX_TRUNCATED" -eq 1 ]]; then
|
|
444
|
+
echo "WARN: Index build completed but looks truncated — see WARN lines above."
|
|
445
|
+
else
|
|
446
|
+
echo "INFO: Index built successfully"
|
|
447
|
+
fi
|
|
448
|
+
echo "INFO: Symbols: ${SYMBOL_COUNT}"
|
|
449
|
+
echo "INFO: Imports: ${IMPORT_COUNT}"
|
|
450
|
+
echo "INFO: Callers: ${CALLER_COUNT}"
|
|
451
|
+
echo "INFO: Files: ${FILE_COUNT}"
|
|
452
|
+
echo "INFO: Cache: ${CACHE_DIR}"
|
|
453
|
+
echo ""
|
|
454
|
+
cat "${CACHE_DIR}/index.json"
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/conformance-check.mjs — FORGE Annotation Protocol conformance suite (#1293)
|
|
4
|
+
*
|
|
5
|
+
* Validates FORGE annotation output (from any producer — Claude Code, Codex adapter,
|
|
6
|
+
* Cursor, Aider, or fixtures) against the FORGE Annotation Protocol v1.0 as defined
|
|
7
|
+
* in docs/spec/forge-protocol-v1.md.
|
|
8
|
+
*
|
|
9
|
+
* This script is the conformance surface referenced in #1293: once run against
|
|
10
|
+
* real or fixture-captured Codex-adapter-produced annotations, it closes the loop
|
|
11
|
+
* on the "second implementation, not just a second document" vendor-neutrality claim.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node scripts/conformance-check.mjs <input.md|input.json> # validate from file
|
|
15
|
+
* node scripts/conformance-check.mjs - # read comment bodies from stdin (one per line JSON or raw text)
|
|
16
|
+
* echo '<!-- FORGE:INVESTIGATOR -->\n...' | node scripts/conformance-check.mjs
|
|
17
|
+
*
|
|
18
|
+
* Input format:
|
|
19
|
+
* - A plain text file containing one or more FORGE annotation blocks (as they would
|
|
20
|
+
* appear in a GitHub issue/PR comment body)
|
|
21
|
+
* - A JSON file: array of comment body strings (e.g. from `gh api .../comments --jq '[.[].body]'`)
|
|
22
|
+
* - stdin: same formats
|
|
23
|
+
*
|
|
24
|
+
* Exit codes:
|
|
25
|
+
* 0 = all annotations conform (or only unknown types found — tolerant by spec §7.2)
|
|
26
|
+
* 1 = one or more conformance violations found
|
|
27
|
+
* 2 = input parse error
|
|
28
|
+
*
|
|
29
|
+
* Conformance rules:
|
|
30
|
+
* This script does NOT reimplement the FORGE Annotation Protocol parsing/validation
|
|
31
|
+
* rules itself. It delegates entirely to the spec reference implementation,
|
|
32
|
+
* `@forgedock/protocol` (packages/protocol/src/{parse,validate,types}.js), which
|
|
33
|
+
* implements §3.1 (opening tag format), §3.2 (completion sentinels), §3.3 (partial
|
|
34
|
+
* sentinels), §4.x (per-type required fields and enum values), and §7.2 (tolerant
|
|
35
|
+
* handling of unknown/unreserved annotation types). Delegating avoids a second,
|
|
36
|
+
* independently-drifting parser (see #1521 — a prior standalone regex here diverged
|
|
37
|
+
* from the library and rejected valid `TYPE:COMPLETE` sentinels as new annotations).
|
|
38
|
+
*
|
|
39
|
+
* PARTIAL sentinel exception: `validate()` reports `valid: false` for an annotation
|
|
40
|
+
* explicitly marked `:PARTIAL` — that strictness (§3.2 "MUST NOT treat as complete")
|
|
41
|
+
* is correct for library consumers deciding whether data is safe to consume, and is
|
|
42
|
+
* locked in by packages/protocol/test/validate.test.mjs and fixtures/context-partial.json.
|
|
43
|
+
* This CLI's pass/fail gate is a narrower question ("should CI exit non-zero for this
|
|
44
|
+
* input?") and, consistent with its pre-#1521 behavior, treats an explicit PARTIAL as
|
|
45
|
+
* a WARN, not a failure — the producer was honest about the interruption (§3.3). Only
|
|
46
|
+
* a fully missing/interrupted sentinel (no COMPLETE, no PARTIAL) fails the exit code.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { readFileSync } from "fs";
|
|
50
|
+
import { parse, validate } from "../packages/protocol/src/index.js";
|
|
51
|
+
import { SentinelState } from "../packages/protocol/src/types.js";
|
|
52
|
+
|
|
53
|
+
// ── Line-number recovery ──────────────────────────────────────────────────────
|
|
54
|
+
//
|
|
55
|
+
// parse() returns ParsedAnnotation objects ({ type, raw, body, ... }) without a
|
|
56
|
+
// line number — it only needs character-level text, not position, for its own
|
|
57
|
+
// purposes. This script's console output has always reported a 1-indexed line
|
|
58
|
+
// number per annotation, so we recover it here by matching each annotation's
|
|
59
|
+
// opening-tag line against the original comment body, scanning forward from the
|
|
60
|
+
// end of the previous match. Annotations are contiguous in the source (parse()
|
|
61
|
+
// assigns every line from an opening tag onward to that annotation until the
|
|
62
|
+
// next true opening tag), so a single forward-scanning pass is sufficient.
|
|
63
|
+
function withStartLines(commentBody, annotations) {
|
|
64
|
+
const lines = commentBody.split("\n");
|
|
65
|
+
let searchFrom = 0;
|
|
66
|
+
return annotations.map((ann) => {
|
|
67
|
+
const openingLine = ann.raw.split("\n")[0];
|
|
68
|
+
let idx = lines.indexOf(openingLine, searchFrom);
|
|
69
|
+
if (idx === -1) idx = searchFrom;
|
|
70
|
+
searchFrom = idx + 1;
|
|
71
|
+
return { ...ann, startLine: idx + 1 };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Input parsing ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function parseInput(raw) {
|
|
78
|
+
// Try JSON array of comment body strings
|
|
79
|
+
const trimmed = raw.trim();
|
|
80
|
+
if (trimmed.startsWith("[")) {
|
|
81
|
+
try {
|
|
82
|
+
const arr = JSON.parse(trimmed);
|
|
83
|
+
if (Array.isArray(arr)) return arr.map(String);
|
|
84
|
+
} catch (_) { /* fall through */ }
|
|
85
|
+
}
|
|
86
|
+
// Single JSON string
|
|
87
|
+
if (trimmed.startsWith('"')) {
|
|
88
|
+
try { return [JSON.parse(trimmed)]; } catch (_) { /* fall through */ }
|
|
89
|
+
}
|
|
90
|
+
// Raw text — treat as single comment body
|
|
91
|
+
return [raw];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const args = process.argv.slice(2);
|
|
97
|
+
let raw;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
if (args.length === 0 || args[0] === "-") {
|
|
101
|
+
// Read stdin via fd 0 rather than the "/dev/stdin" path — the latter does not
|
|
102
|
+
// exist on Windows (ENOENT), breaking the piped-stdin mode documented above
|
|
103
|
+
// despite it being an advertised, supported input mode. Reading fd 0 directly
|
|
104
|
+
// is the portable idiom and works identically on POSIX. (forge#1594)
|
|
105
|
+
raw = readFileSync(0, "utf8");
|
|
106
|
+
} else {
|
|
107
|
+
raw = readFileSync(args[0], "utf8");
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error(`conformance-check: cannot read input — ${e.message}`);
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const commentBodies = parseInput(raw);
|
|
115
|
+
if (!commentBodies.length) {
|
|
116
|
+
console.error("conformance-check: no comment bodies to validate");
|
|
117
|
+
process.exit(2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let totalAnnotations = 0;
|
|
121
|
+
let totalViolations = 0;
|
|
122
|
+
let totalWarnings = 0;
|
|
123
|
+
|
|
124
|
+
for (const [idx, body] of commentBodies.entries()) {
|
|
125
|
+
const parsed = parse(body);
|
|
126
|
+
if (!parsed.length) continue;
|
|
127
|
+
|
|
128
|
+
const annotations = withStartLines(body, parsed);
|
|
129
|
+
|
|
130
|
+
for (const ann of annotations) {
|
|
131
|
+
totalAnnotations++;
|
|
132
|
+
const { errors, warnings } = validate(ann);
|
|
133
|
+
|
|
134
|
+
const prefix = `[comment ${idx + 1}, line ${ann.startLine}] FORGE:${ann.type}`;
|
|
135
|
+
|
|
136
|
+
// An explicit PARTIAL sentinel is an honest producer signal (§3.3), not a silent
|
|
137
|
+
// contract violation — demote validate()'s PARTIAL-specific error out of this CLI's
|
|
138
|
+
// exit-code tally. validate() already emits an equivalent entry in `warnings` for
|
|
139
|
+
// PARTIAL annotations, so dropping the error here (rather than re-printing it) avoids
|
|
140
|
+
// duplicate WARN lines for the same condition. Any other error on the same annotation
|
|
141
|
+
// (missing required field, bad enum value, etc.) is unaffected and still fails.
|
|
142
|
+
const hardErrors =
|
|
143
|
+
ann.sentinelState === SentinelState.PARTIAL
|
|
144
|
+
? errors.filter((e) => !/is marked PARTIAL/.test(e))
|
|
145
|
+
: errors;
|
|
146
|
+
|
|
147
|
+
if (hardErrors.length === 0 && warnings.length === 0) {
|
|
148
|
+
console.log(`PASS ${prefix}`);
|
|
149
|
+
} else {
|
|
150
|
+
for (const v of hardErrors) {
|
|
151
|
+
console.error(`FAIL ${prefix} — ${v}`);
|
|
152
|
+
totalViolations++;
|
|
153
|
+
}
|
|
154
|
+
for (const w of warnings) {
|
|
155
|
+
console.warn(`WARN ${prefix} — ${w}`);
|
|
156
|
+
totalWarnings++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`\nSummary: ${totalAnnotations} annotation(s), ${totalViolations} violation(s), ${totalWarnings} warning(s)`);
|
|
163
|
+
|
|
164
|
+
if (totalAnnotations === 0) {
|
|
165
|
+
console.log("No FORGE annotations found in input.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
process.exit(totalViolations > 0 ? 1 : 0);
|