session-orchestrator 3.22.0 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/hooks/subagent-telemetry.mjs +11 -26
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +517 -18
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +220 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +20 -9
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +54 -17
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-validator-registration.mjs — every `scripts/lib/validate/check-*.mjs`
|
|
4
|
+
* must be referenced by basename from at least one of the three surfaces
|
|
5
|
+
* that actually RUN a validator (`scripts/validate-plugin.mjs`,
|
|
6
|
+
* `.husky/pre-commit`, `.gitlab-ci.yml`), or declare itself deliberately
|
|
7
|
+
* standalone. #1184.
|
|
8
|
+
*
|
|
9
|
+
* THE CLASS. A checker with real detection logic and zero callers is built
|
|
10
|
+
* work nobody ever runs — this repo's own `check-unwired-features.mjs`
|
|
11
|
+
* census exists for a sibling shape of the same problem (config keys nobody
|
|
12
|
+
* reads); this checker is that same discipline applied to the checkers
|
|
13
|
+
* directory itself. `scripts/validate-plugin.mjs` is the canonical local
|
|
14
|
+
* runner (`runCheck('check-foo.mjs')`); `.husky/pre-commit` and
|
|
15
|
+
* `.gitlab-ci.yml` are the two OTHER surfaces some checkers wire into
|
|
16
|
+
* directly instead of (or in addition to) the orchestrator —
|
|
17
|
+
* `check-owner-leakage.mjs`, `check-test-fixture-shapes.mjs` and
|
|
18
|
+
* `check-test-value-bans.mjs` all do this (verified live at HEAD 2ccea0f2).
|
|
19
|
+
*
|
|
20
|
+
* OPT-OUT. A checker that is deliberately CLI-only (invoked by a human or a
|
|
21
|
+
* different tool, never by these three surfaces) declares itself with a
|
|
22
|
+
* header-comment marker: `// registration: standalone <reason>`. The marker
|
|
23
|
+
* makes the checker report PASS as "standalone", not merely silence a
|
|
24
|
+
* warning — the same shape of inline self-declared exemption
|
|
25
|
+
* `check-untracked-test-deps.mjs`'s `IGNORE_MARKER` and
|
|
26
|
+
* `check-dead-bridge.mjs`'s `:ignore` marker already use in this directory.
|
|
27
|
+
*
|
|
28
|
+
* ORACLE. A plain substring match of the checker's own basename (e.g.
|
|
29
|
+
* `"check-foo.mjs"`) against the COMMENT-STRIPPED text of the three surface
|
|
30
|
+
* files — the same granularity `runCheck('check-foo.mjs')` calls and
|
|
31
|
+
* `.husky`/CI script lines already use to name a checker. MEASURED (HEAD
|
|
32
|
+
* 2ccea0f2, all 33 live `check-*.mjs` basenames): zero basenames are a
|
|
33
|
+
* substring of another, so this match cannot cross-attribute one checker's
|
|
34
|
+
* registration to a different one.
|
|
35
|
+
*
|
|
36
|
+
* COMMENT-STRIPPING (HIGH, qa review, #1184 FX-C). A basename referenced
|
|
37
|
+
* ONLY inside a `//`/`#` line comment or a `/* *\/` block comment is NOT a
|
|
38
|
+
* real registration — the surface text is stripped of comments (quote-aware,
|
|
39
|
+
* so a `#`/`//` INSIDE a string — a URL fragment, a shell parameter
|
|
40
|
+
* expansion `${VAR#pattern}` — is never mistaken for a comment start) before
|
|
41
|
+
* matching. MEASURED before this fix: a fixture whose only reference to
|
|
42
|
+
* `check-ghost.mjs` sat in `// runCheck('check-ghost.mjs'); // DISABLED`
|
|
43
|
+
* reported `registered: true` (exit 0) — commenting a checker OUT silently
|
|
44
|
+
* kept it PASSing. `scripts/validate-plugin.mjs` uses `//`+`/* *\/` (js);
|
|
45
|
+
* `.husky/pre-commit` and `.gitlab-ci.yml` use `#` (sh/yaml) — see
|
|
46
|
+
* {@link commentStyleForSurface}. The checker's OWN
|
|
47
|
+
* `// registration: standalone` header marker is read from the CHECKER file
|
|
48
|
+
* directly (never from a surface text) and is unaffected by this stripping.
|
|
49
|
+
*
|
|
50
|
+
* NAMED CEILING (BV-004): a checker referenced only in prose (a `.md` doc, a
|
|
51
|
+
* skill body) and nowhere in the three RUN surfaces above still reports
|
|
52
|
+
* UNREGISTERED — being documented is not being run. REVISIT if a fourth run
|
|
53
|
+
* surface (a new CI job file, a different git hook) is ever added: extend
|
|
54
|
+
* `RUN_SURFACES`, do not special-case it here. The comment stripper's own
|
|
55
|
+
* quote-tracking is a single flat state — an escaped quote (`\"`) inside a
|
|
56
|
+
* double-quoted string is not honoured, and a template-literal's `${...}`
|
|
57
|
+
* interpolation is not walked separately. Both failure directions lean
|
|
58
|
+
* toward treating MORE text as "inside a string" than a real parser would,
|
|
59
|
+
* which can only make the stripper MISS a comment (false "still
|
|
60
|
+
* registered"), never manufacture a false UNREGISTERED — the direction this
|
|
61
|
+
* checker's own false-positive history (the paragraph above) already
|
|
62
|
+
* measured as the live hazard.
|
|
63
|
+
*
|
|
64
|
+
* Usage: check-validator-registration.mjs <repo-root>
|
|
65
|
+
* Output: ` PASS: …` / ` FAIL: …` lines (two leading spaces), then
|
|
66
|
+
* `Results: N passed, M failed`. Exit 0 = every checker registered or
|
|
67
|
+
* standalone, 1 = at least one unregistered checker, 2 = tool error.
|
|
68
|
+
*
|
|
69
|
+
* Import-safety: importing this module MUST NOT execute anything — the
|
|
70
|
+
* isMain guard at the bottom is the only side-effecting path.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
74
|
+
import path from 'node:path';
|
|
75
|
+
import { pathToFileURL } from 'node:url';
|
|
76
|
+
import { listRepoFiles } from './repo-files.mjs';
|
|
77
|
+
|
|
78
|
+
/** Marker line inside a checker's own header — declares deliberate CLI-only status. */
|
|
79
|
+
export const STANDALONE_MARKER = /^\s*\/\/\s*registration:\s*standalone\b(?:\s+(.*))?$/m;
|
|
80
|
+
|
|
81
|
+
/** The three surfaces that actually RUN a validator (not merely mention it). */
|
|
82
|
+
const RUN_SURFACES = Object.freeze([
|
|
83
|
+
path.join('scripts', 'validate-plugin.mjs'),
|
|
84
|
+
path.join('.husky', 'pre-commit'),
|
|
85
|
+
path.join('.gitlab-ci.yml'),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const VALIDATE_DIR_REL = path.join('scripts', 'lib', 'validate');
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Comment style for a RUN_SURFACES path, keyed on extension rather than
|
|
92
|
+
* position — avoids a silent index-drift if `RUN_SURFACES` is ever
|
|
93
|
+
* reordered. `.mjs` gets JS-shaped comments (`//`, `/* *\/`, quotes
|
|
94
|
+
* `'`/`"`/`` ` ``); everything else (`.husky/pre-commit` has no extension,
|
|
95
|
+
* `.gitlab-ci.yml`) gets shell/YAML-shaped comments (`#` only, quotes
|
|
96
|
+
* `'`/`"`).
|
|
97
|
+
*
|
|
98
|
+
* @param {string} rel repo-relative surface path
|
|
99
|
+
* @returns {{lineComment: string, blockComment: boolean, quoteChars: string[]}}
|
|
100
|
+
*/
|
|
101
|
+
function commentStyleForSurface(rel) {
|
|
102
|
+
return rel.endsWith('.mjs')
|
|
103
|
+
? { lineComment: '//', blockComment: true, quoteChars: ['"', "'", '`'] }
|
|
104
|
+
: { lineComment: '#', blockComment: false, quoteChars: ['"', "'"] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Strip comments from `text` so a checker basename mentioned only inside a
|
|
109
|
+
* comment is never read as a real registration. Quote-aware: walks `'`/`"`
|
|
110
|
+
* (and, for the js style, `` ` ``) spans without inspecting their contents,
|
|
111
|
+
* so a comment marker INSIDE a string (a URL fragment `#frag`, a shell
|
|
112
|
+
* parameter expansion `${VAR#pattern}`) is left untouched rather than
|
|
113
|
+
* truncating the line early. See the header NAMED CEILING for what this
|
|
114
|
+
* quote-tracking deliberately does not attempt.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} text
|
|
117
|
+
* @param {{lineComment: string, blockComment: boolean, quoteChars: string[]}} style
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
export function stripComments(text, { lineComment, blockComment, quoteChars }) {
|
|
121
|
+
let out = '';
|
|
122
|
+
let i = 0;
|
|
123
|
+
let inQuote = null;
|
|
124
|
+
while (i < text.length) {
|
|
125
|
+
const ch = text[i];
|
|
126
|
+
if (inQuote) {
|
|
127
|
+
out += ch;
|
|
128
|
+
if (ch === '\\' && i + 1 < text.length) {
|
|
129
|
+
out += text[i + 1];
|
|
130
|
+
i += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === inQuote) inQuote = null;
|
|
134
|
+
i += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (quoteChars.includes(ch)) {
|
|
138
|
+
inQuote = ch;
|
|
139
|
+
out += ch;
|
|
140
|
+
i += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (blockComment && ch === '/' && text[i + 1] === '*') {
|
|
144
|
+
const end = text.indexOf('*/', i + 2);
|
|
145
|
+
i = end === -1 ? text.length : end + 2;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (text.startsWith(lineComment, i)) {
|
|
149
|
+
const nl = text.indexOf('\n', i);
|
|
150
|
+
i = nl === -1 ? text.length : nl; // keep the newline itself, drop the comment text
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
out += ch;
|
|
154
|
+
i += 1;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @typedef {{basename: string, registered: boolean, standalone: boolean, surfaces: string[]}} RegistrationResult
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} repoRoot
|
|
165
|
+
* @returns {RegistrationResult[]} sorted by basename
|
|
166
|
+
*/
|
|
167
|
+
export function scanValidatorRegistration(repoRoot) {
|
|
168
|
+
const checkerFiles = listRepoFiles(repoRoot, { dirs: [VALIDATE_DIR_REL], exts: ['mjs'] }).filter(
|
|
169
|
+
(f) => /^check-.*\.mjs$/.test(path.basename(f)),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Comment-stripped before matching (HIGH, #1184 FX-C): a basename
|
|
173
|
+
// referenced only inside a `//`/`#`/`/* *\/` comment is NOT a real
|
|
174
|
+
// registration — see the header's COMMENT-STRIPPING paragraph.
|
|
175
|
+
const surfaceTexts = RUN_SURFACES.map((rel) => {
|
|
176
|
+
const abs = path.join(repoRoot, rel);
|
|
177
|
+
try {
|
|
178
|
+
const raw = existsSync(abs) ? readFileSync(abs, 'utf8') : '';
|
|
179
|
+
return stripComments(raw, commentStyleForSurface(rel));
|
|
180
|
+
} catch {
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/** @type {RegistrationResult[]} */
|
|
186
|
+
const results = [];
|
|
187
|
+
for (const abs of checkerFiles) {
|
|
188
|
+
const basename = path.basename(abs);
|
|
189
|
+
let source = '';
|
|
190
|
+
try {
|
|
191
|
+
source = readFileSync(abs, 'utf8');
|
|
192
|
+
} catch {
|
|
193
|
+
/* unreadable — no marker can be found, no surface reference can save it either */
|
|
194
|
+
}
|
|
195
|
+
const standalone = STANDALONE_MARKER.test(source);
|
|
196
|
+
const surfaces = RUN_SURFACES.filter((_, i) => surfaceTexts[i].includes(basename));
|
|
197
|
+
results.push({ basename, registered: surfaces.length > 0, standalone, surfaces });
|
|
198
|
+
}
|
|
199
|
+
return results.sort((a, b) => a.basename.localeCompare(b.basename));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// CLI
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Run the check against a repo root, printing the validate-plugin line
|
|
208
|
+
* vocabulary.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} repoRoot
|
|
211
|
+
* @returns {number} 0 = every checker registered or standalone, 1 = finding(s), 2 = tool error
|
|
212
|
+
*/
|
|
213
|
+
export function runCheckValidatorRegistration(repoRoot) {
|
|
214
|
+
console.log('--- Check: validator registration (check-*.mjs must be wired or standalone) ---');
|
|
215
|
+
|
|
216
|
+
const results = scanValidatorRegistration(repoRoot);
|
|
217
|
+
let pass = 0;
|
|
218
|
+
let fail = 0;
|
|
219
|
+
|
|
220
|
+
for (const r of results) {
|
|
221
|
+
if (r.standalone) {
|
|
222
|
+
console.log(` PASS: ${r.basename} — declared standalone (registration: standalone)`);
|
|
223
|
+
pass += 1;
|
|
224
|
+
} else if (r.registered) {
|
|
225
|
+
console.log(` PASS: ${r.basename} — referenced in ${r.surfaces.join(', ')}`);
|
|
226
|
+
pass += 1;
|
|
227
|
+
} else {
|
|
228
|
+
console.log(
|
|
229
|
+
` FAIL: ${r.basename} — referenced by NEITHER scripts/validate-plugin.mjs, .husky/pre-commit, NOR .gitlab-ci.yml, and carries no "registration: standalone" marker`,
|
|
230
|
+
);
|
|
231
|
+
fail += 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log('');
|
|
236
|
+
console.log(`Results: ${pass} passed, ${fail} failed`);
|
|
237
|
+
return fail > 0 ? 1 : 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
|
|
241
|
+
if (isMain) {
|
|
242
|
+
const root = process.argv[2];
|
|
243
|
+
if (!root) {
|
|
244
|
+
console.error('Usage: check-validator-registration.mjs <repo-root>');
|
|
245
|
+
process.exit(2);
|
|
246
|
+
}
|
|
247
|
+
process.exit(runCheckValidatorRegistration(path.resolve(root)));
|
|
248
|
+
}
|
|
@@ -194,6 +194,7 @@
|
|
|
194
194
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
195
195
|
import path from 'node:path';
|
|
196
196
|
import { pathToFileURL } from 'node:url';
|
|
197
|
+
import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
|
|
197
198
|
|
|
198
199
|
/** Directories whose content is scanned. Root-level `*.md` is added separately. */
|
|
199
200
|
const SCAN_DIRS = Object.freeze([
|
|
@@ -224,9 +225,6 @@ const DOC_EXTENSIONS = Object.freeze(['.md']);
|
|
|
224
225
|
/** Code extensions (comment + string-literal rules apply). */
|
|
225
226
|
const CODE_EXTENSIONS = Object.freeze(['.mjs', '.js', '.cjs']);
|
|
226
227
|
|
|
227
|
-
/** Fence languages whose body is shell. Anything else is prose. */
|
|
228
|
-
const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
|
|
229
|
-
|
|
230
228
|
/**
|
|
231
229
|
* Real top-level subcommands, harvested from `gh --help` / `glab --help`
|
|
232
230
|
* (gh 2.86.0 / glab 1.91.0, 2026-08-14). Filter 4: a token that is not in this
|
|
@@ -576,33 +574,13 @@ export function scanMarkdown(relative, body, tally) {
|
|
|
576
574
|
const findings = [];
|
|
577
575
|
/** @type {{line: number, text: string}[]} */
|
|
578
576
|
const shellLines = [];
|
|
579
|
-
/** @type {{marker: string, length: number, shell: boolean} | null} */
|
|
580
|
-
let fence = null;
|
|
581
577
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const raw = lines[index];
|
|
585
|
-
const fenceMatch = raw.match(/^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/);
|
|
586
|
-
if (fenceMatch) {
|
|
587
|
-
const marker = fenceMatch[1][0];
|
|
588
|
-
const length = fenceMatch[1].length;
|
|
589
|
-
const lang = fenceMatch[2].toLowerCase();
|
|
590
|
-
if (fence === null) {
|
|
591
|
-
fence = { marker, length, shell: SHELL_LANGS.has(lang) };
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
// A closing fence uses the same char, is at least as long, and has no info string.
|
|
595
|
-
if (marker === fence.marker && length >= fence.length && lang === '') {
|
|
596
|
-
fence = null;
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
// Otherwise it is fence content (a nested fence inside a wider one).
|
|
600
|
-
}
|
|
601
|
-
if (fence === null || !fence.shell) continue;
|
|
578
|
+
forEachLine(body, (raw, { lineNumber, inFence, lang }) => {
|
|
579
|
+
if (!inFence || !SHELL_LANGS.has(lang)) return;
|
|
602
580
|
const stripped = raw.replace(/^\s*[$❯>]\s+/, '');
|
|
603
|
-
if (/^\s*#/.test(stripped))
|
|
604
|
-
shellLines.push({ line:
|
|
605
|
-
}
|
|
581
|
+
if (/^\s*#/.test(stripped)) return;
|
|
582
|
+
shellLines.push({ line: lineNumber, text: stripped });
|
|
583
|
+
});
|
|
606
584
|
|
|
607
585
|
for (const entry of joinContinuations(shellLines)) {
|
|
608
586
|
for (const hit of extractBareInvocations(entry.text, tally)) {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-fences.mjs — the ONE fenced-code-block tracker shared by every
|
|
3
|
+
* Markdown-scanning validator/extractor in this repo (#1181).
|
|
4
|
+
*
|
|
5
|
+
* ## Why one module
|
|
6
|
+
*
|
|
7
|
+
* The same regex pair + open/close comparison had drifted into four separate
|
|
8
|
+
* copies — `check-doc-cli-commands.mjs`, `check-skill-script-paths.mjs`,
|
|
9
|
+
* `check-vcs-repo-flag.mjs` and `auq/parse.mjs` — each tracking fence depth
|
|
10
|
+
* and language independently. A drifted copy is a silent one: nothing fails
|
|
11
|
+
* when copy #3 diverges from copy #1, because each copy only has to agree
|
|
12
|
+
* with itself.
|
|
13
|
+
*
|
|
14
|
+
* ## The rule this module encodes
|
|
15
|
+
*
|
|
16
|
+
* A fence opens on a line beginning with 3+ backticks or 3+ tildes, carrying
|
|
17
|
+
* an optional info string (most commonly a language tag). It closes on a
|
|
18
|
+
* line whose marker CHARACTER matches, whose LENGTH is at least the
|
|
19
|
+
* opener's, and whose info string is empty — CommonMark reserves the info
|
|
20
|
+
* string for the OPENING fence only, so a fence-shaped line that still
|
|
21
|
+
* carries one is fence CONTENT (most often a nested fence one level in), not
|
|
22
|
+
* a closer.
|
|
23
|
+
*
|
|
24
|
+
* ## The one real divergence between the four original copies, preserved as a parameter
|
|
25
|
+
*
|
|
26
|
+
* Three of the four copies anchor the fence-line regex only at the START of
|
|
27
|
+
* the line — trailing text after the info string is simply not captured,
|
|
28
|
+
* never rejected. `auq/parse.mjs`'s copy anchors at BOTH ends: a line
|
|
29
|
+
* carrying anything past the info string besides trailing whitespace is not
|
|
30
|
+
* recognised as a fence line at all. This is `{ wholeLine: true }` below —
|
|
31
|
+
* a real behavioural difference, not stylistic, so it stays a caller-chosen
|
|
32
|
+
* option rather than being silently resolved one way. Every other
|
|
33
|
+
* consumer-specific behaviour (the shell-language predicate, blockquote
|
|
34
|
+
* stripping, unbalanced-fence reporting) likewise stays at the call site —
|
|
35
|
+
* this module owns only the fence-line grammar itself.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** Fence languages whose body is shell. Both `check-doc-cli-commands.mjs` and
|
|
39
|
+
* `check-vcs-repo-flag.mjs` filtered on this identical literal set before
|
|
40
|
+
* extraction; centralised here rather than kept as two copies of the same
|
|
41
|
+
* five strings. */
|
|
42
|
+
export const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
|
|
43
|
+
|
|
44
|
+
/** Start-anchored: matches CommonMark, tolerates trailing info-string text. */
|
|
45
|
+
const FENCE_LINE_START_RE = /^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/;
|
|
46
|
+
|
|
47
|
+
/** Whole-line-anchored: nothing but whitespace may follow the info string. */
|
|
48
|
+
const FENCE_LINE_WHOLE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*([^\s`~]*)[ \t]*$/u;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Match a candidate fence-marker line.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} line
|
|
54
|
+
* @param {{ wholeLine?: boolean }} [options] `wholeLine: true` requires the
|
|
55
|
+
* ENTIRE line (after the marker) to be nothing but the info string plus
|
|
56
|
+
* trailing whitespace — the stricter reading `auq/parse.mjs` needs.
|
|
57
|
+
* Default `false` only anchors the START of the line, matching
|
|
58
|
+
* CommonMark and the three `check-*.mjs` validators.
|
|
59
|
+
* @returns {{ marker: string, length: number, info: string } | null}
|
|
60
|
+
*/
|
|
61
|
+
export function matchFenceLine(line, { wholeLine = false } = {}) {
|
|
62
|
+
const match = (wholeLine ? FENCE_LINE_WHOLE_RE : FENCE_LINE_START_RE).exec(line);
|
|
63
|
+
if (!match) return null;
|
|
64
|
+
return { marker: match[1][0], length: match[1].length, info: match[2] ?? '' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Does `candidate` close the fence opened by `open`? Same marker character,
|
|
69
|
+
* at least as long, and carrying no info string of its own.
|
|
70
|
+
*
|
|
71
|
+
* @param {{ marker: string, length: number }} open
|
|
72
|
+
* @param {{ marker: string, length: number, info: string }} candidate
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
export function closesFence(open, candidate) {
|
|
76
|
+
return candidate.marker === open.marker && candidate.length >= open.length && candidate.info === '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Normalise a fence's info string into a comparable language tag. Every
|
|
81
|
+
* caller that classified a fence by language did `.toLowerCase()` (never
|
|
82
|
+
* `.trim()`, since the capturing regex cannot include leading/trailing
|
|
83
|
+
* whitespace in the first place) before comparing against a language set —
|
|
84
|
+
* this makes that normalisation a single, explicit step instead of an
|
|
85
|
+
* implicit property of the extraction regex.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} info
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeLang(info) {
|
|
91
|
+
return String(info ?? '').trim().toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Strip a leading blockquote `>` chain so a quoted fence (`> \`\`\``) is
|
|
96
|
+
* still recognised as a fence line. Only `check-skill-script-paths.mjs`
|
|
97
|
+
* needs this — a fence inside a blockquote is still a fence there, but the
|
|
98
|
+
* other callers never scan quoted content.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} line
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
export function stripBlockquote(line) {
|
|
104
|
+
return line.replace(/^(?:\s*>)+\s?/, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Walk `text` line by line, tracking fence state, and call `onLine` for
|
|
109
|
+
* every CONTENT line. Fence-marker lines themselves (the opener and the
|
|
110
|
+
* closer) are consumed by the tracker and never handed to the callback —
|
|
111
|
+
* all four original copies treated marker lines as structural, never as
|
|
112
|
+
* scannable content. A fence-shaped line that neither opens nor validly
|
|
113
|
+
* closes (a nested fence one level in) IS still content and reaches the
|
|
114
|
+
* callback, exactly as in the original copies.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} text
|
|
117
|
+
* @param {(line: string, state: { lineNumber: number, inFence: boolean, lang: string | null }) => void} onLine
|
|
118
|
+
* @param {{ wholeLine?: boolean, stripBlockquotes?: boolean }} [options]
|
|
119
|
+
* @returns {{ unbalancedFenceLine: number | null }} the 1-based line of a
|
|
120
|
+
* fence that opened and never closed by EOF, or `null` if every fence
|
|
121
|
+
* this walk saw was balanced.
|
|
122
|
+
*/
|
|
123
|
+
export function forEachLine(text, onLine, options = {}) {
|
|
124
|
+
const { wholeLine = false, stripBlockquotes = false } = options;
|
|
125
|
+
const lines = text.split('\n');
|
|
126
|
+
/** @type {{ marker: string, length: number, openLine: number } | null} */
|
|
127
|
+
let fence = null;
|
|
128
|
+
let lang = null;
|
|
129
|
+
|
|
130
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
131
|
+
const raw = lines[index];
|
|
132
|
+
const probe = stripBlockquotes ? stripBlockquote(raw) : raw;
|
|
133
|
+
const candidate = matchFenceLine(probe, { wholeLine });
|
|
134
|
+
if (candidate) {
|
|
135
|
+
if (fence === null) {
|
|
136
|
+
fence = { marker: candidate.marker, length: candidate.length, openLine: index + 1 };
|
|
137
|
+
lang = normalizeLang(candidate.info);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (closesFence(fence, candidate)) {
|
|
141
|
+
fence = null;
|
|
142
|
+
lang = null;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// Otherwise it is fence content (a nested fence inside a wider one) — falls through.
|
|
146
|
+
}
|
|
147
|
+
onLine(raw, { lineNumber: index + 1, inFence: fence !== null, lang });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { unbalancedFenceLine: fence ? fence.openLine : null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract fence BLOCKS (an open/close pair plus its body) rather than a
|
|
155
|
+
* per-line walk — the shape `auq/parse.mjs` needs, since it iterates fences
|
|
156
|
+
* as units, not lines.
|
|
157
|
+
*
|
|
158
|
+
* An unbalanced fence (opens, never closes) contributes NO block, mirroring
|
|
159
|
+
* the original `fencesOf()` contract: it silently drops the dangling opener
|
|
160
|
+
* rather than reporting it. Unlike `check-skill-script-paths.mjs`, whose
|
|
161
|
+
* unterminated fence is a doc DEFECT worth its own finding, `auq/parse.mjs`
|
|
162
|
+
* scans AskUserQuestion blocks in production code and prose, where an
|
|
163
|
+
* unterminated fence was never treated as a reportable condition in its own
|
|
164
|
+
* right — only as "no block found here".
|
|
165
|
+
*
|
|
166
|
+
* @param {string} text
|
|
167
|
+
* @param {{ wholeLine?: boolean }} [options]
|
|
168
|
+
* @returns {Array<{ openLine: number, closeLine: number, lang: string, bodyLines: string[], bodyStartLine: number }>}
|
|
169
|
+
*/
|
|
170
|
+
export function scanFenceBlocks(text, options = {}) {
|
|
171
|
+
const { wholeLine = false } = options;
|
|
172
|
+
const lines = text.split('\n');
|
|
173
|
+
/** @type {Array<{ openLine: number, closeLine: number, lang: string, bodyLines: string[], bodyStartLine: number }>} */
|
|
174
|
+
const blocks = [];
|
|
175
|
+
/** @type {{ marker: string, length: number, info: string, startIdx: number } | null} */
|
|
176
|
+
let open = null;
|
|
177
|
+
|
|
178
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
179
|
+
const candidate = matchFenceLine(lines[index], { wholeLine });
|
|
180
|
+
if (!candidate) continue;
|
|
181
|
+
if (open === null) {
|
|
182
|
+
open = { ...candidate, startIdx: index };
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!closesFence(open, candidate)) continue; // fence content (nested), not a close
|
|
186
|
+
blocks.push({
|
|
187
|
+
openLine: open.startIdx + 1,
|
|
188
|
+
closeLine: index + 1,
|
|
189
|
+
lang: open.info,
|
|
190
|
+
bodyLines: lines.slice(open.startIdx + 1, index),
|
|
191
|
+
bodyStartLine: open.startIdx + 2,
|
|
192
|
+
});
|
|
193
|
+
open = null;
|
|
194
|
+
}
|
|
195
|
+
return blocks;
|
|
196
|
+
}
|