@polymorphism-tech/morph-spec 9.0.0 → 9.1.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.md +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +4 -62
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +11 -67
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +34 -12
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +14 -96
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +28 -275
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Mirrors risk-detector.js's style: pure, deterministic, no I/O. This module
|
|
5
|
-
* is the TESTABLE MIRROR of the natural-language classification rules that
|
|
6
|
-
* live in MORPH.md — behavior described here must stay in sync with those
|
|
7
|
-
* rules, but this file is what `create`/`retype` call at runtime.
|
|
8
|
-
*
|
|
9
|
-
* Precedence: hotfix keywords win outright (a production incident never
|
|
10
|
-
* ties down against a bug or chore signal). Otherwise bug vs. chore is
|
|
11
|
-
* decided by keyword-match count, nudged by two weak signals:
|
|
12
|
-
* - estimatedFiles <= 2 -> +1 toward bug (small, surgical diffs)
|
|
13
|
-
* - declaredScope <= 6 words -> +1 toward chore (a narrowly-scoped ask)
|
|
14
|
-
* A genuine tie between bug and chore is surfaced to the caller (LLM/HCOP
|
|
15
|
-
* router) rather than guessed at. Zero keyword matches -> no opinion; the
|
|
16
|
-
* caller defaults to `feature`.
|
|
17
|
-
*/
|
|
1
|
+
// Testable mirror of the classification rules in MORPH.md: the two must stay in sync.
|
|
2
|
+
// Hotfix wins outright; a bug/chore tie is returned to the caller, never guessed.
|
|
18
3
|
|
|
19
4
|
// Plural/inflected forms are listed EXPLICITLY: matching is whole-word, so
|
|
20
5
|
// "incidentes" is not "incidente". Recall lives here, in a list that is visible
|
|
@@ -37,38 +22,8 @@ const CHORE_KEYWORDS = [
|
|
|
37
22
|
'limpeza', 'docs', 'documentação', 'lint', 'formatação',
|
|
38
23
|
];
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* words (or whole phrases), never as a fragment of a larger word.
|
|
43
|
-
*
|
|
44
|
-
* Substring matching routed real requests to the wrong pipeline: `p0` is two
|
|
45
|
-
* characters with outright hotfix precedence, so "endpoint group0" matched it
|
|
46
|
-
* and skipped Gates 1 and 2 by policy — no human, and not even the LLM router,
|
|
47
|
-
* since a substring hit reports `confidence: 'high'`. Same shape for `fix`
|
|
48
|
-
* inside "prefix"/"fixtures" and `bug` inside "debug".
|
|
49
|
-
*
|
|
50
|
-
* The failure directions are asymmetric, which is what settles the trade-off: a
|
|
51
|
-
* false POSITIVE silently skips approval gates, while a false negative usually
|
|
52
|
-
* only lowers confidence — and low confidence defers to the LLM router (exit 2).
|
|
53
|
-
* Matching strictly is therefore the safe error, and provably so for the vector
|
|
54
|
-
* that motivated it: whole-word matches are a strict SUBSET of substring matches
|
|
55
|
-
* per keyword, so narrowing can never invent a hotfix hit that substring didn't
|
|
56
|
-
* already have. No misroute INTO the only gate-skipping pipeline can be created
|
|
57
|
-
* here, only removed.
|
|
58
|
-
*
|
|
59
|
-
* Being precise about the limit of that argument, since overclaiming in a
|
|
60
|
-
* comment is what this file is fixing: the asymmetry holds per KEYWORD, not per
|
|
61
|
-
* CLASSIFICATION. Dropping a match can still shift the bug-vs-chore balance —
|
|
62
|
-
* losing a bug hit may promote a chore from `tie` to `high`, and chore also
|
|
63
|
-
* skips Gates 1 and 2. Whole-word matching DID cost recall that substring had:
|
|
64
|
-
* "bugs" contains "bug" but is not the word "bug". The answer is to list the
|
|
65
|
-
* forms (below), not to match loosely — recall belongs in the vocabulary, where
|
|
66
|
-
* it is explicit and testable, not in a matcher that fires on fragments.
|
|
67
|
-
*
|
|
68
|
-
* `\b` is not usable here: JS word boundaries are ASCII-only, so `\bnão\b`
|
|
69
|
-
* would find a boundary between `n` and `ã` and misbehave on half of these
|
|
70
|
-
* pt-BR keywords. Unicode letter/number lookarounds are the portable form.
|
|
71
|
-
*/
|
|
25
|
+
// Whole words only: a false positive silently skips approval gates (`p0` in "group0", `fix` in
|
|
26
|
+
// "prefix"). `\b` is ASCII-only in JS and breaks on pt-BR letters, hence the Unicode lookarounds.
|
|
72
27
|
function matchKeywords(haystack, keywords) {
|
|
73
28
|
return keywords.filter((kw) => {
|
|
74
29
|
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -81,18 +36,6 @@ function wordCount(str) {
|
|
|
81
36
|
return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
|
|
82
37
|
}
|
|
83
38
|
|
|
84
|
-
/**
|
|
85
|
-
* @param {object} input
|
|
86
|
-
* @param {string} [input.text]
|
|
87
|
-
* @param {string} [input.declaredScope]
|
|
88
|
-
* @param {number} [input.estimatedFiles]
|
|
89
|
-
* @returns {{
|
|
90
|
-
* workType: string|null,
|
|
91
|
-
* confidence: 'high'|'tie'|'none',
|
|
92
|
-
* matched: { hotfix: string[], bug: string[], chore: string[], feature: string[] },
|
|
93
|
-
* candidates: string[],
|
|
94
|
-
* }}
|
|
95
|
-
*/
|
|
96
39
|
export function detectWorkType({ text = '', declaredScope = '', estimatedFiles } = {}) {
|
|
97
40
|
const haystack = `${text} ${declaredScope}`.toLowerCase();
|
|
98
41
|
|
|
@@ -107,7 +50,6 @@ export function detectWorkType({ text = '', declaredScope = '', estimatedFiles }
|
|
|
107
50
|
feature: [], // no keywords of its own — the catch-all default
|
|
108
51
|
};
|
|
109
52
|
|
|
110
|
-
// Hard precedence: any hotfix signal wins outright, never ties down.
|
|
111
53
|
if (hotfixMatched.length > 0) {
|
|
112
54
|
return { workType: 'hotfix', confidence: 'high', matched, candidates: ['hotfix'] };
|
|
113
55
|
}
|
|
@@ -119,11 +61,7 @@ export function detectWorkType({ text = '', declaredScope = '', estimatedFiles }
|
|
|
119
61
|
let bugScore = bugMatched.length;
|
|
120
62
|
let choreScore = choreMatched.length;
|
|
121
63
|
|
|
122
|
-
//
|
|
123
|
-
// keyword counts are already equal. Otherwise a clean single-keyword match
|
|
124
|
-
// on one type would be dragged into a tie by the other type's weak signal
|
|
125
|
-
// (e.g. a lone "bug" keyword + a short declaredScope), routing an
|
|
126
|
-
// unambiguous request to the LLM and risking a gate-skipping misroute.
|
|
64
|
+
// Weak nudges only break ties: applied otherwise, they drag a clean single-keyword match into a tie.
|
|
127
65
|
if (bugMatched.length === choreMatched.length) {
|
|
128
66
|
if (typeof estimatedFiles === 'number' && estimatedFiles <= 2) {
|
|
129
67
|
bugScore += 1;
|
|
@@ -1,41 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* cannot answer: the docker stacks still running for them.
|
|
4
|
-
*
|
|
5
|
-
* `worktree-audit.js` reconciles git's registry, the disk and morph's features.
|
|
6
|
-
* None of the three can see a CONTAINER. A stack survives everything they look
|
|
7
|
-
* at: `finish` removes the worktree and the branch, the directory disappears,
|
|
8
|
-
* git forgets the registration — and `morph-{feature}` keeps running, holding
|
|
9
|
-
* the feature's whole port block (and its named volumes) against the next
|
|
10
|
-
* worktree that lands on the same block. The failure surfaces later, somewhere
|
|
11
|
-
* else, as `port is already allocated` during an `e2e up` that has nothing to
|
|
12
|
-
* do with the stack that is actually holding the port.
|
|
13
|
-
*
|
|
14
|
-
* Two findings, both `severity: 'warn'` — reported, never a failure of
|
|
15
|
-
* `doctor --worktrees` (`doctor.js` treats `info`/`warn` as non-problems):
|
|
16
|
-
*
|
|
17
|
-
* • docker-stack-orphan — a `morph-*` compose project of THIS repository
|
|
18
|
-
* running for a feature this tree does not know
|
|
19
|
-
* • docker-port-held — a port from a feature's allocated block published
|
|
20
|
-
* by a container belonging to a DIFFERENT compose project
|
|
21
|
-
*
|
|
22
|
-
* This module does not go into `doctor --full`, on purpose (D10): every check
|
|
23
|
-
* there must be satisfiable in the framework repo itself, and a docker check in
|
|
24
|
-
* a repo with no compose file could only be permanently yellow. A check that
|
|
25
|
-
* can never pass where it runs teaches nothing.
|
|
26
|
-
*
|
|
27
|
-
* Nothing here mutates anything — not the registry it reads (see
|
|
28
|
-
* `allocatedFeatures`), and certainly not docker. The `fix` strings name the
|
|
29
|
-
* destructive command; a human runs it.
|
|
30
|
-
*
|
|
31
|
-
* @module lib/worktree/docker-audit
|
|
32
|
-
*/
|
|
33
|
-
|
|
1
|
+
// Stays out of `doctor --full`: a docker check in a repo with no compose file
|
|
2
|
+
// could only be permanently yellow. Read-only; the `fix` strings are for a human.
|
|
34
3
|
import { execFileSync } from 'child_process';
|
|
35
4
|
import { resolve } from 'path';
|
|
36
5
|
|
|
37
|
-
// src/ → framework/hooks/shared/ is the allowed direction (hooks are installed
|
|
38
|
-
// standalone into target projects, where src/ does not exist).
|
|
39
6
|
import {
|
|
40
7
|
composeProjectName,
|
|
41
8
|
defaultExec,
|
|
@@ -44,19 +11,9 @@ import {
|
|
|
44
11
|
} from '../../../framework/hooks/shared/e2e-check.js';
|
|
45
12
|
import { allocateEnvBlock, allocatedFeatures, portLayout, resolveFleetConfig } from './env-allocator.js';
|
|
46
13
|
|
|
47
|
-
/**
|
|
48
|
-
* The prefix `composeProjectName()` stamps on every stack morph brings up,
|
|
49
|
-
* derived from the function itself rather than retyped — the string `morph-`
|
|
50
|
-
* living in two places is how the two stop agreeing.
|
|
51
|
-
*/
|
|
52
14
|
const PROJECT_PROBE = 'probe';
|
|
53
15
|
const MORPH_PROJECT_PREFIX = composeProjectName(PROJECT_PROBE).slice(0, -PROJECT_PROBE.length);
|
|
54
16
|
|
|
55
|
-
/**
|
|
56
|
-
* Same fail-open resolution the other callers use: the COMMON git dir is the
|
|
57
|
-
* one location every linked worktree shares, and it is where the port-block
|
|
58
|
-
* registry lives.
|
|
59
|
-
*/
|
|
60
17
|
function resolveGitCommonDir(cwd) {
|
|
61
18
|
try {
|
|
62
19
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
|
|
@@ -68,31 +25,12 @@ function resolveGitCommonDir(cwd) {
|
|
|
68
25
|
}
|
|
69
26
|
}
|
|
70
27
|
|
|
71
|
-
|
|
72
|
-
* Path comparison that matches the host's own rules. Windows paths differ in
|
|
73
|
-
* drive-letter case between what git reports and what docker echoes back;
|
|
74
|
-
* treating those as different paths would make every stack look foreign.
|
|
75
|
-
*
|
|
76
|
-
* Exported as the ONE copy of that rule: `doctor.js` compares a declared test
|
|
77
|
-
* node `cwd` with a discovered `package.json` folder through it.
|
|
78
|
-
*/
|
|
28
|
+
// On Windows git and docker report the drive letter in different case.
|
|
79
29
|
export const samePathCase = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
|
|
80
30
|
const norm = (p) => samePathCase(resolve(String(p)).replace(/\\/g, '/'));
|
|
81
31
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*
|
|
85
|
-
* The `morph-` prefix is global to the host, not to a repository: two different
|
|
86
|
-
* repos that use morph-spec and both have a feature named `triagem` produce the
|
|
87
|
-
* SAME compose project name. Without this test, `doctor` in repo A would report
|
|
88
|
-
* repo B's live stack as an orphan and hand the reader a `down -v` — volumes
|
|
89
|
-
* included — for somebody else's running database. The compose file's path is
|
|
90
|
-
* what ties a stack to a repository, and it keeps pointing inside the root even
|
|
91
|
-
* after the worktree directory it named is gone.
|
|
92
|
-
*
|
|
93
|
-
* Unattributable (no config file in the listing) counts as NOT ours: an
|
|
94
|
-
* unknown owner is not evidence of ownership.
|
|
95
|
-
*/
|
|
32
|
+
// The `morph-` prefix is host-global: two repos with the same feature name share
|
|
33
|
+
// a project name, so ownership comes from the compose file path. No path = not ours.
|
|
96
34
|
function belongsToRoot(entry, rootNorm) {
|
|
97
35
|
const configFiles = String(entry?.ConfigFiles ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
98
36
|
if (configFiles.length === 0) return false;
|
|
@@ -105,14 +43,6 @@ function belongsToRoot(entry, rootNorm) {
|
|
|
105
43
|
});
|
|
106
44
|
}
|
|
107
45
|
|
|
108
|
-
/**
|
|
109
|
-
* Host ports a `docker ps`/`docker compose ps` entry publishes.
|
|
110
|
-
* Both output shapes are accepted rather than pinning a docker version:
|
|
111
|
-
* `Ports` as the human string (`0.0.0.0:4002->5432/tcp`) and `Publishers` as
|
|
112
|
-
* the structured array compose emits.
|
|
113
|
-
*
|
|
114
|
-
* @returns {Set<number>}
|
|
115
|
-
*/
|
|
116
46
|
function publishedPorts(entry) {
|
|
117
47
|
const ports = new Set();
|
|
118
48
|
for (const pub of Array.isArray(entry?.Publishers) ? entry.Publishers : []) {
|
|
@@ -127,7 +57,6 @@ function publishedPorts(entry) {
|
|
|
127
57
|
return ports;
|
|
128
58
|
}
|
|
129
59
|
|
|
130
|
-
/** The compose project a container belongs to, from its labels. `null` = unknown. */
|
|
131
60
|
function composeProjectOf(entry) {
|
|
132
61
|
const labels = String(entry?.Labels ?? '');
|
|
133
62
|
for (const part of labels.split(',')) {
|
|
@@ -138,83 +67,43 @@ function composeProjectOf(entry) {
|
|
|
138
67
|
return null;
|
|
139
68
|
}
|
|
140
69
|
|
|
141
|
-
/** Readable container name for a `docker ps` entry (Names may be a list). */
|
|
142
70
|
function containerNameOf(entry) {
|
|
143
71
|
const names = String(entry?.Names ?? entry?.Name ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
144
72
|
return names[0] || String(entry?.ID ?? 'container desconhecido');
|
|
145
73
|
}
|
|
146
74
|
|
|
147
|
-
|
|
148
|
-
* Parse a docker JSON listing that may be an array or NDJSON.
|
|
149
|
-
* `null` means the output could not be read — DISTINCT from `[]`, which means
|
|
150
|
-
* docker answered and there was nothing running. A caller that conflated the
|
|
151
|
-
* two would turn "could not ask" into "nothing to report", or worse, into a
|
|
152
|
-
* finding.
|
|
153
|
-
*
|
|
154
|
-
* @returns {Array<object>|null}
|
|
155
|
-
*/
|
|
75
|
+
// `null` = unreadable, distinct from `[]` = docker answered with nothing running.
|
|
156
76
|
function parseDockerJson(raw) {
|
|
157
77
|
const text = String(raw ?? '').trim();
|
|
158
78
|
if (!text) return [];
|
|
159
79
|
const parsed = parseComposePs(text);
|
|
160
80
|
if (parsed.length > 0) return parsed;
|
|
161
|
-
// parseComposePs
|
|
162
|
-
// very different answers into `[]`: "docker listed nothing" and "this is not
|
|
163
|
-
// JSON". Only a literal empty array is the first one; everything else that
|
|
164
|
-
// parsed to nothing is garbage, and garbage must not read as "all clear".
|
|
81
|
+
// parseComposePs returns `[]` for non-JSON too; only a literal `[]` is "nothing".
|
|
165
82
|
return /^\[\s*\]$/.test(text) ? [] : null;
|
|
166
83
|
}
|
|
167
84
|
|
|
168
85
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* silence is the point: this module is loaded by `doctor --worktrees` in every
|
|
173
|
-
* repo, including ones that never containerize anything.
|
|
174
|
-
*
|
|
175
|
-
* 1. no feature ever allocated a port block (no registry): there is no
|
|
176
|
-
* isolation contract to compare a running stack against;
|
|
177
|
-
* 2. the docker CLI is not usable here;
|
|
178
|
-
* 3. any docker call exits non-zero or answers with something that is not
|
|
179
|
-
* JSON — fail-open, the standing rule of this repo's diagnostics.
|
|
180
|
-
*
|
|
181
|
-
* `[]` here reads as "nothing to report", not as "unknown": with no docker or
|
|
182
|
-
* no blocks, nothing CAN be colliding. That is why this is not the `null`-is-
|
|
183
|
-
* UNKNOWN shape `pr-index.js` uses — there the question stays open, here the
|
|
184
|
-
* absence is itself the answer.
|
|
185
|
-
*
|
|
186
|
-
* @param {string} root - primary root of the repository
|
|
187
|
-
* @param {object} [opts]
|
|
188
|
-
* @param {(file: string, args: string[], o?: object) => {code: number, output: string}} [opts.exec]
|
|
189
|
-
* injectable executor, exactly like `e2e-check.js` — the CI runners have no
|
|
190
|
-
* guaranteed daemon, so every path here must be testable without docker
|
|
191
|
-
* @param {Iterable<string>} [opts.features] - feature names this tree knows.
|
|
192
|
-
* Passed IN rather than re-derived: `auditWorktrees` already read
|
|
193
|
-
* `.morph/features/` and `git worktree list`, and two scans of the same fact
|
|
194
|
-
* are two chances to disagree about whether a stack is orphaned
|
|
195
|
-
* @param {string|null} [opts.gitCommonDir]
|
|
196
|
-
* @returns {Array<{type: string, severity: string, detail: string, fix: string, feature?: string}>}
|
|
86
|
+
* `[]` means "nothing to report", not unknown: with no port block allocated or
|
|
87
|
+
* no docker, nothing can collide. Docker errors are fail-open.
|
|
88
|
+
* `features` is passed in so the audit and `auditWorktrees` share one scan.
|
|
197
89
|
*/
|
|
198
90
|
export function auditDockerStacks(root, { exec = defaultExec, features = [], gitCommonDir } = {}) {
|
|
199
91
|
const commonDir = gitCommonDir === undefined ? resolveGitCommonDir(root) : gitCommonDir;
|
|
200
92
|
if (!commonDir) return [];
|
|
201
93
|
|
|
202
|
-
// (1) No port block was ever allocated → no fleet isolation to protect.
|
|
203
94
|
const allocated = allocatedFeatures(commonDir);
|
|
204
95
|
if (allocated.length === 0) return [];
|
|
205
96
|
|
|
206
|
-
// (2) No docker → nothing can be holding anything.
|
|
207
97
|
if (!isDockerAvailable(root, exec)) return [];
|
|
208
98
|
|
|
209
99
|
const known = new Set(features);
|
|
210
100
|
const rootNorm = norm(root);
|
|
211
101
|
const issues = [];
|
|
212
102
|
|
|
213
|
-
// ---- docker-stack-orphan ----------------------------------------------
|
|
214
103
|
const lsResult = exec('docker', ['compose', 'ls', '--format', 'json'], { cwd: root });
|
|
215
|
-
if (lsResult.code !== 0) return [];
|
|
104
|
+
if (lsResult.code !== 0) return [];
|
|
216
105
|
const projects = parseDockerJson(lsResult.output);
|
|
217
|
-
if (projects === null) return [];
|
|
106
|
+
if (projects === null) return [];
|
|
218
107
|
|
|
219
108
|
const knownProjects = new Set([...known].map((f) => composeProjectName(f)));
|
|
220
109
|
for (const entry of projects) {
|
|
@@ -229,35 +118,27 @@ export function auditDockerStacks(root, { exec = defaultExec, features = [], git
|
|
|
229
118
|
severity: 'warn',
|
|
230
119
|
feature: slug || null,
|
|
231
120
|
detail: `stack docker "${name}" está de pé (${String(entry?.Status ?? 'estado desconhecido')}) e não corresponde a nenhuma feature desta árvore — sobra de outra sessão, segurando o bloco de portas e os volumes de "${slug}"`,
|
|
232
|
-
//
|
|
233
|
-
// compose file, and this stack's feature no longer exists here. The
|
|
234
|
-
// project name is the only handle that still works.
|
|
121
|
+
// Not `e2e down {slug}`: the feature's compose file no longer resolves.
|
|
235
122
|
fix: `docker compose -p ${name} down -v`,
|
|
236
123
|
});
|
|
237
124
|
}
|
|
238
125
|
|
|
239
|
-
// ---- docker-port-held --------------------------------------------------
|
|
240
126
|
const psResult = exec('docker', ['ps', '--format', 'json'], { cwd: root });
|
|
241
|
-
if (psResult.code !== 0) return [];
|
|
127
|
+
if (psResult.code !== 0) return [];
|
|
242
128
|
const containers = parseDockerJson(psResult.output);
|
|
243
129
|
if (containers === null) return [];
|
|
244
130
|
|
|
245
131
|
const { basePort, blockSize } = resolveFleetConfig(root);
|
|
246
132
|
for (const feature of allocated) {
|
|
247
|
-
// A block allocated by a feature this tree does not know belongs to another
|
|
248
|
-
// worktree of the repo; its collisions are that worktree's to report.
|
|
249
133
|
if (!known.has(feature)) continue;
|
|
250
134
|
|
|
251
135
|
let layout;
|
|
252
136
|
try {
|
|
253
|
-
//
|
|
254
|
-
// the early-return path and writes nothing. The expected ports are
|
|
255
|
-
// DERIVED from portLayout — the same function that writes worktree.env
|
|
256
|
-
// and the compose env — never recomputed here.
|
|
137
|
+
// Feature is already in the registry, so allocateEnvBlock writes nothing.
|
|
257
138
|
const block = allocateEnvBlock(feature, { gitCommonDir: commonDir, basePort, blockSize });
|
|
258
139
|
layout = portLayout(block, { feature });
|
|
259
140
|
} catch {
|
|
260
|
-
continue;
|
|
141
|
+
continue;
|
|
261
142
|
}
|
|
262
143
|
|
|
263
144
|
const ownProject = layout.COMPOSE_PROJECT_NAME;
|
|
@@ -269,7 +150,7 @@ export function auditDockerStacks(root, { exec = defaultExec, features = [], git
|
|
|
269
150
|
|
|
270
151
|
for (const container of containers) {
|
|
271
152
|
const project = composeProjectOf(container);
|
|
272
|
-
if (project === ownProject) continue;
|
|
153
|
+
if (project === ownProject) continue;
|
|
273
154
|
for (const port of publishedPorts(container)) {
|
|
274
155
|
const key = expected.get(port);
|
|
275
156
|
if (!key) continue;
|
|
@@ -1,34 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Two parallel features that both start `next dev` (or `dotnet run`) collide on
|
|
5
|
-
* the same default port, and the failure is invisible until one server kills
|
|
6
|
-
* the other. Worktrees isolate files and branches, not runtime — this module
|
|
7
|
-
* closes that gap by giving every feature worktree its own stable block of
|
|
8
|
-
* ports.
|
|
9
|
-
*
|
|
10
|
-
* The registry lives at `<git-common-dir>/morph-spec/env-registry.json` — under
|
|
11
|
-
* `.git/` on purpose, for the same reasons as `finish-backup/`: it can never
|
|
12
|
-
* dirty the `git status` the worktree-finish gates read, it survives
|
|
13
|
-
* `git clean -fdx`, and the COMMON dir is the one location every linked
|
|
14
|
-
* worktree of the repo shares.
|
|
15
|
-
*
|
|
16
|
-
* Allocation is deterministic (first free index), idempotent per feature, and
|
|
17
|
-
* released only when `morph-spec finish` actually removed the worktree. Writes
|
|
18
|
-
* are tmp+rename so a crash never leaves a torn registry; a corrupt registry
|
|
19
|
-
* is treated as empty (fail-open — the cost is a re-allocation, never a crash).
|
|
20
|
-
*/
|
|
1
|
+
// The registry lives under the COMMON git dir: shared by every linked worktree,
|
|
2
|
+
// invisible to the `git status` the finish gates read, and survives `git clean -fdx`.
|
|
21
3
|
|
|
22
4
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
23
5
|
import { join } from 'path';
|
|
24
|
-
// src/ → hooks/shared/ is the allowed direction (hooks are installed standalone).
|
|
25
6
|
import { composeProjectName } from '../../../framework/hooks/shared/e2e-check.js';
|
|
26
7
|
|
|
27
8
|
const DEFAULT_BASE_PORT = 4000;
|
|
28
9
|
const DEFAULT_BLOCK_SIZE = 10;
|
|
29
10
|
const MAX_PORT = 65535;
|
|
30
11
|
|
|
31
|
-
/** Registry file path for a repo's common git dir. */
|
|
32
12
|
export function registryPath(gitCommonDir) {
|
|
33
13
|
return join(gitCommonDir, 'morph-spec', 'env-registry.json');
|
|
34
14
|
}
|
|
@@ -40,7 +20,7 @@ function readRegistry(gitCommonDir) {
|
|
|
40
20
|
if (parsed && typeof parsed.allocations === 'object' && parsed.allocations !== null) {
|
|
41
21
|
return parsed;
|
|
42
22
|
}
|
|
43
|
-
} catch { /* fail-open:
|
|
23
|
+
} catch { /* fail-open: a re-allocation, never a crash */ }
|
|
44
24
|
return { version: 1, allocations: {} };
|
|
45
25
|
}
|
|
46
26
|
|
|
@@ -52,13 +32,6 @@ function writeRegistry(gitCommonDir, registry) {
|
|
|
52
32
|
renameSync(tmp, path);
|
|
53
33
|
}
|
|
54
34
|
|
|
55
|
-
/**
|
|
56
|
-
* Read fleet overrides from the project's committed config.json.
|
|
57
|
-
* Fail-open: missing/malformed config (or a Windows BOM) → defaults.
|
|
58
|
-
*
|
|
59
|
-
* @param {string} rootPath - primary root of the project
|
|
60
|
-
* @returns {{ basePort: number, blockSize: number }}
|
|
61
|
-
*/
|
|
62
35
|
export function resolveFleetConfig(rootPath) {
|
|
63
36
|
const defaults = { basePort: DEFAULT_BASE_PORT, blockSize: DEFAULT_BLOCK_SIZE };
|
|
64
37
|
try {
|
|
@@ -74,15 +47,7 @@ export function resolveFleetConfig(rootPath) {
|
|
|
74
47
|
}
|
|
75
48
|
}
|
|
76
49
|
|
|
77
|
-
/**
|
|
78
|
-
* Allocate (or return the existing) port block for a feature.
|
|
79
|
-
* First-free-index policy: released slots are reused, so long-lived repos
|
|
80
|
-
* don't creep toward the port ceiling.
|
|
81
|
-
*
|
|
82
|
-
* @param {string} feature
|
|
83
|
-
* @param {{ gitCommonDir: string, basePort?: number, blockSize?: number }} opts
|
|
84
|
-
* @returns {{ index: number, base: number, range: [number, number], created: boolean }}
|
|
85
|
-
*/
|
|
50
|
+
/** First free index wins, so released slots are reused instead of creeping toward the port ceiling. */
|
|
86
51
|
export function allocateEnvBlock(feature, { gitCommonDir, basePort = DEFAULT_BASE_PORT, blockSize = DEFAULT_BLOCK_SIZE }) {
|
|
87
52
|
const registry = readRegistry(gitCommonDir);
|
|
88
53
|
|
|
@@ -110,23 +75,7 @@ export function allocateEnvBlock(feature, { gitCommonDir, basePort = DEFAULT_BAS
|
|
|
110
75
|
return { index, base, range: [base, base + blockSize - 1], created: true };
|
|
111
76
|
}
|
|
112
77
|
|
|
113
|
-
/**
|
|
114
|
-
* The features that ALREADY own a block — read-only, allocating nothing.
|
|
115
|
-
*
|
|
116
|
-
* `allocateEnvBlock` is the only sanctioned way to get a block, and it WRITES
|
|
117
|
-
* when the feature has none. A diagnostic that has to ask "who owns a block?"
|
|
118
|
-
* must not answer by handing one out: `doctor` is read-only, and an audit that
|
|
119
|
-
* mutates the registry it audits invents the very state it then reports. So
|
|
120
|
-
* this reader exists to bound the question, and the block itself still comes
|
|
121
|
-
* from `allocateEnvBlock` (which, for a feature listed here, takes the
|
|
122
|
-
* early-return path and writes nothing).
|
|
123
|
-
*
|
|
124
|
-
* An absent or corrupt registry yields `[]` — "nobody allocated anything",
|
|
125
|
-
* which for every caller is the same as "nothing to compare against".
|
|
126
|
-
*
|
|
127
|
-
* @param {string} gitCommonDir
|
|
128
|
-
* @returns {string[]} feature names with an allocated block
|
|
129
|
-
*/
|
|
78
|
+
/** Read-only on purpose: `allocateEnvBlock` writes, and an audit that allocates invents the state it reports. */
|
|
130
79
|
export function allocatedFeatures(gitCommonDir) {
|
|
131
80
|
const registry = readRegistry(gitCommonDir);
|
|
132
81
|
return Object.entries(registry.allocations)
|
|
@@ -134,13 +83,6 @@ export function allocatedFeatures(gitCommonDir) {
|
|
|
134
83
|
.map(([feature]) => feature);
|
|
135
84
|
}
|
|
136
85
|
|
|
137
|
-
/**
|
|
138
|
-
* Release a feature's block. Returns false when nothing was allocated.
|
|
139
|
-
*
|
|
140
|
-
* @param {string} feature
|
|
141
|
-
* @param {{ gitCommonDir: string }} opts
|
|
142
|
-
* @returns {boolean}
|
|
143
|
-
*/
|
|
144
86
|
export function releaseEnvBlock(feature, { gitCommonDir }) {
|
|
145
87
|
const registry = readRegistry(gitCommonDir);
|
|
146
88
|
if (!registry.allocations[feature]) return false;
|
|
@@ -149,27 +91,12 @@ export function releaseEnvBlock(feature, { gitCommonDir }) {
|
|
|
149
91
|
return true;
|
|
150
92
|
}
|
|
151
93
|
|
|
152
|
-
|
|
153
|
-
* Fixed offsets inside a block. The block has 10 ports; four are named, the
|
|
154
|
-
* rest (+4..+9) are the project's to use. A new offset added here MUST also be
|
|
155
|
-
* published by the compose template (`${VAR:-default}:container`), or it is a
|
|
156
|
-
* port nobody listens on.
|
|
157
|
-
*/
|
|
94
|
+
// A new offset here must also be published by the compose template, or it is a port nobody listens on.
|
|
158
95
|
export const PORT_OFFSETS = Object.freeze({ web: 0, api: 1, postgres: 2, redis: 3 });
|
|
159
96
|
|
|
160
97
|
/**
|
|
161
|
-
* The
|
|
162
|
-
*
|
|
163
|
-
* `docker compose` (`src/lib/e2e/compose-env.js`) — so a worktree's file and
|
|
164
|
-
* its running stack can never describe different worlds.
|
|
165
|
-
*
|
|
166
|
-
* `COMPOSE_PROJECT_NAME` is part of the layout on purpose: a hand-run
|
|
167
|
-
* `docker compose up` inside the worktree must land in the isolated project,
|
|
168
|
-
* not in the root's. Every value is a string (env-safe); key order is stable.
|
|
169
|
-
*
|
|
170
|
-
* @param {{ base: number, range: [number, number] }} block
|
|
171
|
-
* @param {{ stack?: string, feature?: string, aliases?: Record<string, string> }} [opts] - aliases: resolvePortAliases()
|
|
172
|
-
* @returns {Record<string,string>}
|
|
98
|
+
* The single layout both `worktree.env` and the compose env derive from. `COMPOSE_PROJECT_NAME`
|
|
99
|
+
* is in it so a hand-run `docker compose up` in the worktree lands in the isolated project.
|
|
173
100
|
*/
|
|
174
101
|
export function portLayout(block, { stack = '', feature = '', aliases = {} } = {}) {
|
|
175
102
|
const env = {};
|
|
@@ -194,9 +121,6 @@ const RESERVED_ENV_NAMES = new Set([
|
|
|
194
121
|
'MORPH_FEATURE',
|
|
195
122
|
]);
|
|
196
123
|
|
|
197
|
-
// e2e.portEnvNames: extra env names for the same four ports, so a compose file reading
|
|
198
|
-
// ${PG_PORT} is fed by the block, not by its own default. An entry that cannot be
|
|
199
|
-
// honoured is dropped and named in `problems`.
|
|
200
124
|
export function resolvePortAliases(config) {
|
|
201
125
|
const declared = config?.e2e?.portEnvNames;
|
|
202
126
|
const aliases = {};
|
|
@@ -217,20 +141,8 @@ export function resolvePortAliases(config) {
|
|
|
217
141
|
}
|
|
218
142
|
|
|
219
143
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* cannot drift from what compose receives.
|
|
223
|
-
*
|
|
224
|
-
* `morphFeature` is for a task tree born from `morph/<f>`: `MORPH_FEATURE`
|
|
225
|
-
* names the feature it works for, while the block and `COMPOSE_PROJECT_NAME`
|
|
226
|
-
* stay keyed by `feature` (`task/<n>`) — keying them by `<f>` would hand the
|
|
227
|
-
* sub-agent the feature's own ports and compose project.
|
|
228
|
-
*
|
|
229
|
-
* @param {string} feature - the env-registry key: `<feature>` or `task/<name>`
|
|
230
|
-
* @param {{ base: number, range: [number, number] }} block
|
|
231
|
-
* @param {string} [stack] - config.json `project.stack` (e.g. "nextjs+dotnet")
|
|
232
|
-
* @param {{ morphFeature?: string|null, aliases?: Record<string, string> }} [opts]
|
|
233
|
-
* @returns {string}
|
|
144
|
+
* `morphFeature` names the feature a `task/<n>` tree works for; the block and compose
|
|
145
|
+
* project stay keyed by `feature`, or the sub-agent would get the feature's own ports.
|
|
234
146
|
*/
|
|
235
147
|
export function buildEnvFileContent(feature, block, stack = '', { morphFeature = null, aliases = {} } = {}) {
|
|
236
148
|
const lines = [
|