forgedock 1.1.1 → 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.
Files changed (171) hide show
  1. package/README.md +53 -8
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/demo.mjs +92 -7
  4. package/bin/engine/phases.mjs +20 -0
  5. package/bin/engine/state.mjs +8 -3
  6. package/bin/engine-cli.mjs +177 -5
  7. package/bin/forgedock.mjs +409 -26
  8. package/bin/hooks/interactive-engine.mjs +509 -0
  9. package/bin/hooks/pre-tool-use.mjs +498 -0
  10. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  11. package/bin/journey.mjs +72 -14
  12. package/bin/registry.mjs +139 -0
  13. package/bin/report.mjs +447 -0
  14. package/bin/runner.mjs +68 -19
  15. package/bin/settings-hook.mjs +235 -1
  16. package/commands/adopt.md +4 -1
  17. package/commands/analytics.md +4 -1
  18. package/commands/audit-agents.md +110 -27
  19. package/commands/audit.md +20 -1
  20. package/commands/autopilot.md +80 -8
  21. package/commands/changelog.md +2 -1
  22. package/commands/ci-audit.md +2 -1
  23. package/commands/cleanup.md +161 -1
  24. package/commands/compat-audit.md +2 -1
  25. package/commands/deploy-info.md +58 -1
  26. package/commands/design-bench.md +5 -1
  27. package/commands/design-render-critique-loop.md +1 -1
  28. package/commands/diagnose.md +2 -1
  29. package/commands/explain.md +2 -1
  30. package/commands/forge-stats.md +121 -2
  31. package/commands/forgedock-init.md +1 -1
  32. package/commands/geo-audit.md +3 -0
  33. package/commands/incident-response.md +2 -1
  34. package/commands/issue.md +23 -2
  35. package/commands/milestone.md +77 -12
  36. package/commands/optimize.md +2 -1
  37. package/commands/orchestrate/config.md +67 -0
  38. package/commands/orchestrate/phase-1-resolve.md +159 -0
  39. package/commands/orchestrate/phase-2-triage.md +126 -0
  40. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  41. package/commands/orchestrate/phase-3-dependency.md +591 -0
  42. package/commands/orchestrate/phase-4-execution.md +962 -0
  43. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  44. package/commands/orchestrate/phase-6-report.md +198 -0
  45. package/commands/orchestrate/safety.md +62 -0
  46. package/commands/orchestrate.md +25 -2045
  47. package/commands/pipeline-health/config.md +34 -0
  48. package/commands/pipeline-health/phase-1-context.md +180 -0
  49. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  50. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  51. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  52. package/commands/pipeline-health/phase-5-report.md +284 -0
  53. package/commands/pipeline-health/phase-6-summary.md +52 -0
  54. package/commands/pipeline-health.md +25 -2401
  55. package/commands/pipeline-resume.md +24 -1
  56. package/commands/pipeline-status.md +65 -0
  57. package/commands/qa-sweep.md +4 -1
  58. package/commands/quality-gate.md +248 -10
  59. package/commands/replay.md +2 -1
  60. package/commands/review-pr-agents/api.md +135 -0
  61. package/commands/review-pr-agents/auth.md +137 -0
  62. package/commands/review-pr-agents/billing.md +101 -0
  63. package/commands/review-pr-agents/concurrency.md +145 -0
  64. package/commands/review-pr-agents/database.md +103 -0
  65. package/commands/review-pr-agents/frontend.md +89 -0
  66. package/commands/review-pr-agents/infra.md +385 -0
  67. package/commands/review-pr-agents/protocols.md +185 -0
  68. package/commands/review-pr-agents/scraper.md +185 -0
  69. package/commands/review-pr-agents/security.md +406 -0
  70. package/commands/review-pr-agents.md +45 -1722
  71. package/commands/review-pr-staging.md +196 -40
  72. package/commands/review-pr.md +591 -52
  73. package/commands/rollback.md +2 -1
  74. package/commands/scope.md +2 -1
  75. package/commands/security-audit.md +2 -1
  76. package/commands/signal-planner.md +623 -0
  77. package/commands/sync-ecosystem.md +1 -0
  78. package/commands/test-gate.md +17 -1
  79. package/commands/validate.md +2 -1
  80. package/commands/work-on/build/architect.md +28 -0
  81. package/commands/work-on/build/context.md +2 -0
  82. package/commands/work-on/build/implement.md +3 -1
  83. package/commands/work-on/build/validate.md +26 -7
  84. package/commands/work-on/build.md +163 -24
  85. package/commands/work-on/close.md +130 -3
  86. package/commands/work-on/decompose.md +19 -1
  87. package/commands/work-on/investigate.md +117 -2
  88. package/commands/work-on/review.md +24 -4
  89. package/commands/work-on-monolithic.md +7 -2
  90. package/commands/work-on.md +202 -4
  91. package/docs/BRAND.md +14 -7
  92. package/docs/CI.md +2 -2
  93. package/docs/CODEX.md +36 -1
  94. package/docs/CONFIG.md +5 -8
  95. package/docs/FORGE-PROTOCOL.md +15 -1064
  96. package/docs/anthropic-devrel-outreach.md +2 -2
  97. package/docs/articles/command-decomposition-token-savings.md +214 -0
  98. package/docs/articles/model-release-playbook.md +131 -0
  99. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  100. package/docs/claude-breakpoints.json +75 -5
  101. package/docs/design/abc-benchmark.md +1 -1
  102. package/docs/design/design-spec-schema.md +1 -1
  103. package/docs/design/divergent-generation.md +1 -1
  104. package/docs/design/eval-gate-runbook.md +178 -0
  105. package/docs/design/show-hn-draft.md +26 -1
  106. package/docs/design/token-cost-baseline-1298.md +59 -0
  107. package/docs/eval/README.md +117 -0
  108. package/docs/eval/corpus/seed-v1.json +6 -0
  109. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  110. package/docs/site/annotations-explained.md +24 -0
  111. package/docs/site/command-learning-path.md +51 -14
  112. package/docs/site/command-reference.md +339 -113
  113. package/docs/site/for-companies.md +95 -0
  114. package/docs/site/forge-annotation-protocol.md +12 -256
  115. package/docs/site/getting-started.md +43 -4
  116. package/docs/site/github-is-the-memory.md +113 -0
  117. package/docs/site/how-it-works.md +3 -13
  118. package/docs/site/index.md +9 -1
  119. package/docs/spec/eval-run-result.md +147 -0
  120. package/docs/spec/forge-protocol-v1.md +441 -18
  121. package/docs/spec/label-state-machine.md +55 -0
  122. package/docs/spec/review-protocol.md +158 -0
  123. package/docs/spec-graph-schema.md +28 -0
  124. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  125. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  126. package/examples/forgedock-demo/README.md +41 -22
  127. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  128. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  129. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  130. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  131. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  132. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  133. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  134. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  135. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  136. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  137. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  138. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  139. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  140. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  141. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  142. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  143. package/examples/forgedock-demo/src/db.js +36 -11
  144. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  145. package/examples/forgedock-demo/src/server.js +4 -1
  146. package/package.json +3 -1
  147. package/packages/protocol/LICENSE +21 -0
  148. package/packages/protocol/src/cli.js +135 -0
  149. package/packages/protocol/src/emit.js +146 -0
  150. package/packages/protocol/src/index.js +43 -0
  151. package/packages/protocol/src/parse.js +229 -0
  152. package/packages/protocol/src/types.js +171 -0
  153. package/packages/protocol/src/validate.js +122 -0
  154. package/scripts/bench-topology-cost.mjs +372 -0
  155. package/scripts/check-command-docs-drift.sh +198 -0
  156. package/scripts/check-protocol-restatements.sh +176 -0
  157. package/scripts/check-registry/README.md +72 -0
  158. package/scripts/check-registry/manifest.json +6 -0
  159. package/scripts/code-index.sh +454 -0
  160. package/scripts/conformance-check.mjs +168 -0
  161. package/scripts/doctor-pipeline-state.sh +484 -0
  162. package/scripts/eval-gate-baseline.json +17 -0
  163. package/scripts/eval-gate-scorecard.mjs +242 -0
  164. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  165. package/scripts/flaky-quarantine.sh +241 -0
  166. package/scripts/forge-annotation.sh +567 -0
  167. package/scripts/issue-dedup.sh +236 -0
  168. package/scripts/validate-annotation-node.mjs +233 -0
  169. package/scripts/worktree-lifecycle.sh +3 -3
  170. package/templates/devdocs/agent/using-forgedock.md +14 -0
  171. package/docs/site/.vitepress/config.mjs +0 -85
@@ -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);