# forbidden-construct-allowlist.txt — the register of stack-specific constructs
# engine core is still permitted to contain (STORY-344 T-PB-344h, AC-8).
#
# The lint is scripts/construct-lint.sh (package-ci.sh gate 2); the constructs
# it forbids are scripts/forbidden-constructs.txt. A hit outside lib/runners/,
# core/orchestrator/tests/, comment-only lines and this list fails CI.
#
# FORMAT   <path-relative-to-engine-root> :: <construct-id>[,<construct-id>…] :: <ERE matched against the line>
#          Lines starting with # are comments. Every entry MUST carry a reason
#          in the comment block above it. EREs must be POSIX-awk portable: no
#          `{m,n}` intervals, no `\b`/`\s`/`\d`.
#
# THE RULE FOR ADDING ONE
# An entry is either (a) VOCABULARY — core recognising many stacks side by side
# so that it assumes none of them (a classifier table, a closed parser enum), or
# (b) DEBT with a named owner — a stack assumption that is known, scoped and
# scheduled to leave. An assumption with no owner is not an entry; it is a fix.
#
# Anchoring on line content, not line numbers, is deliberate (the vendor-noun
# allowlist's rule): the entry survives the file moving and stops matching the
# moment the line is edited. A stale entry — one that matches no hit — FAILS the
# lint, so an edited line is always re-reviewed rather than silently excused.
#
# WHAT THIS LINT CANNOT SEE — read before treating a green gate as proof.
# A product identifier that is neither a tool, a glob, a manifest nor a layout
# path is invisible by construction: the always-on `nav-integrity` journey in
# lib/prompts/qa-v{1,2}.md.tmpl, and `harness/qa/scan-logs.sh` plus the default
# `factoryos` compose project in core/commands/qa-verify.md, are FactoryOS names
# that pass gate 2 (STORY-344 D-6/D-9 follow-ups; recorded as D-11). They belong
# in `profile.live_stack.journeys` and the QA recipe, not in a word list here.

# ── (a) Vocabulary: multi-stack classifiers ─────────────────────────────────
# baseline-report.sh sorts a client's EXISTING CI commands into categories
# (tests, security, formatting, build) for the onboarding baseline report. It
# names a dozen ecosystems side by side precisely so it assumes none; a command
# it does not recognise falls to "other". Removing a tool here only makes the
# report vaguer for that stack — it never changes what a gate runs.
core/orchestrator/lib/baseline-report.sh :: tool-test-runner :: ^        tests\)      re='test\|spec\|pytest\|vitest\|jest\|e2e' ;;$
core/orchestrator/lib/baseline-report.sh :: tool-package-manager :: ^        security\)   re='
core/orchestrator/lib/baseline-report.sh :: tool-package-manager :: ^        formatting\) re='
core/orchestrator/lib/baseline-report.sh :: tool-package-manager,tool-test-runner :: ^        tests\)      re='\(\^\|\[ /\]\)\(pytest
core/orchestrator/lib/baseline-report.sh :: tool-package-manager :: ^        build\)      re='

# The `counts:` parser dispatch. The parser names are the closed enum in
# sdk/manifests/config.schema.json (junit, phpunit-junit, vitest, pytest): a
# profile suite DECLARES which output format it emits, and core only maps that
# declared name to a text parser. Core never infers the runner.
core/orchestrator/lib/test-counts.sh :: tool-test-runner :: ^    (vitest|pytest)\) _count_(vitest|pytest)_text "\$log" ;;$

# ── (b) Debt: the heavy-test fence classifier ───────────────────────────────
# fence_runs_heavy_tests (lib/fence.sh; moved from orchestrate.sh by STORY-351
# CR-001) decides whether a Verification block's
# command is a whole-suite run that must queue behind the capacity-1 heavy-test
# slot. It does so by parsing pnpm/npm/yarn/turbo and uv/poetry pytest argument
# shapes — real JS/Python knowledge in core, and the largest construct debt left.
# It is an over-approximation that errs toward serializing (a false positive
# costs a wait), so a non-JS/Python stack loses only the serialization, never a
# gate. OWNER/FOLLOW-UP: STORY-344 D-11 — move the shapes behind a profile key
# (`profile.heavy_commands`, EREs, as `live_stack.drivers` did for the live-qa
# regex in T-PB-344d) and delete these entries.
core/orchestrator/lib/fence.sh :: tool-test-runner :: function is_test\(w\) \{ return w ~ /\^\(test\(:\.\*\)\?\|vitest\|jest\)\$/ \}
core/orchestrator/lib/fence.sh :: tool-package-manager :: function heavy\(seg, .*hastest, turbo, k, py, pyscoped\)
core/orchestrator/lib/fence.sh :: tool-package-manager :: if \(w == "--filter" \|\| w == "-F" \|\| w == "--package" \|\| \(mgr == "npm"
core/orchestrator/lib/fence.sh :: tool-package-manager :: if \(mgr == "pnpm" && \(w == "-w" \|\| w == "--workspace-root"\)\)
core/orchestrator/lib/fence.sh :: tool-package-manager,tool-test-runner :: hastest = 0; turbo = \(mgr == "turbo"\); py = \(mgr == "pytest"\); pyscoped = 0
core/orchestrator/lib/fence.sh :: tool-package-manager :: if \(words\[k\] == "turbo"\) turbo = 1
core/orchestrator/lib/fence.sh :: tool-package-manager,tool-test-runner :: if \(words\[k\] == "pytest" && mgr ~ /
core/orchestrator/lib/fence.sh :: tool-package-manager :: if \(mgr == "npm" && words\[1\] == "t"\) hastest = 1
core/orchestrator/lib/fence.sh :: tool-package-manager :: if \(mgr !~ /\^\(pnpm\|npm\|yarn\|npx\|turbo\)\$/ \|\| !hastest\) return 0
core/orchestrator/lib/fence.sh :: tool-package-manager :: if \(turbo \|\| rec\) return 1
core/orchestrator/lib/fence.sh :: tool-package-manager :: if \(mgr == "npx" \|\| dirf \|\| \(pkgcwd && !rootf\)\) return 0
core/orchestrator/lib/fence.sh :: tool-package-manager :: return words\[1\] == "test" \|\| \(words\[1\] == "run" && words\[2\] == "test"\) \|\| \(mgr == "npm" && words\[1\] == "t"\)

# The run-copy find prune list. Copying a dependency tree into a run directory
# is never wanted, and `node_modules` is the one tree big enough to matter; a
# missing prune for another ecosystem costs disk, not correctness.
# OWNER/FOLLOW-UP: STORY-344 D-11 — read the prune list from the profile's
# dependency directories with this one as the default.
core/orchestrator/lib/runner-lifecycle.sh :: path-app-layout :: -not -path '\*/\.git/\*' -not -path '\*/node_modules/\*'

# ── (b) Debt: the shipped command manual and templates ──────────────────────
# Prose a consuming project copies into its agent's command directory. It still
# describes FactoryOS's TS + Python stack — the same CONTENT debt the vendor-noun
# allowlist records for these files, and owned by the same follow-up: the manual
# rewrite in STORY-118's standalone-engine repackaging. Engine behaviour does not
# read any of these lines.
core/commands/generate-tests.md :: tool-test-runner :: ^\| \*\*Pure functions / utilities\*\* \| vitest \(TS\) / pytest \(Py\)
core/commands/generate-tests.md :: tool-test-runner :: ^\| \*\*Service code \(controllers, services\)\*\* \| vitest\+supertest \(TS\) / pytest\+httpx \(Py\)
core/commands/generate-tests.md :: tool-test-runner :: ^\| \*\*Frontend components\*\* \| vitest \+ testing-library \+ Playwright
core/commands/handoff.md :: tool-package-manager :: ^   - `uv run mypy --strict` \(if any Python touched\)$
core/commands/handoff.md :: tool-package-manager,tool-test-runner :: ^   - `uv run pytest` \(if any Python touched\)$
core/commands/handoff.md :: tool-package-manager :: ^   - `uv run ruff check` \(if any Python touched\)$
core/commands/handoff.md :: tool-test-runner :: ^   - pytest: <n> passed, <n> failed$
core/commands/qa-story.md :: tool-stack-cli :: NAMING the service under the qa project: `docker compose -p <project>-qa up$

# bug.md's Verification guidance shows a worked fence and the repo-root path
# trap with a real example (STORY-249/BUG-006). The example's stack is
# illustrative; the rule it teaches (paths resolve from the repo root) is not.
core/templates/bug.md :: path-app-layout :: so declaring `services/` blocks every other service task\. -->$
core/templates/bug.md :: path-app-layout :: ^         cd "\$\(git rev-parse --show-toplevel\)/apps/web"$
core/templates/bug.md :: tool-package-manager,tool-test-runner,path-app-layout :: ^         npx vitest run src/__tests__/the-regression\.test\.tsx$
core/templates/bug.md :: path-app-layout :: PATHS RESOLVE FROM THE REPO ROOT the fence starts in\. `cd apps/web` then$
core/templates/bug.md :: path-app-layout :: `--config \.\./harness/\.\.\.` resolves to `apps/harness/\.\.\.` and the bug$
