@polymorphism-tech/morph-spec 8.47.0 → 9.0.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 +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- 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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
|
@@ -44,8 +44,8 @@ import {
|
|
|
44
44
|
getActiveFeature,
|
|
45
45
|
getFeaturePhase,
|
|
46
46
|
getPendingGates,
|
|
47
|
-
getTaskStats,
|
|
48
47
|
} from '../../shared/state-reader.js';
|
|
48
|
+
import { sliceProgress } from '../../shared/slice-progress.js';
|
|
49
49
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
50
50
|
import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
|
|
51
51
|
import { rotateActivityLogIfLarge } from '../../shared/activity-logger.js';
|
|
@@ -232,10 +232,11 @@ function buildActiveFeatureSummary(dir) {
|
|
|
232
232
|
if (!active) return null;
|
|
233
233
|
|
|
234
234
|
const phase = getFeaturePhase(active.name, dir);
|
|
235
|
-
const
|
|
235
|
+
const slices = sliceProgress(join(dir, '.morph', 'features', active.name));
|
|
236
236
|
const pending = getPendingGates(active.name, dir);
|
|
237
237
|
|
|
238
|
-
let text = `MORPH-SPEC: feature ativa ${active.name} (fase ${phase})
|
|
238
|
+
let text = `MORPH-SPEC: feature ativa ${active.name} (fase ${phase})`;
|
|
239
|
+
if (slices) text += ` · fatias ${slices.done}/${slices.total}`;
|
|
239
240
|
if (pending.length) {
|
|
240
241
|
text += ` · gates pendentes: ${pending.join(', ')}`;
|
|
241
242
|
}
|
|
@@ -264,7 +265,7 @@ function claimBlock(dir, feature, sessionId) {
|
|
|
264
265
|
if (!other) return '';
|
|
265
266
|
return `\n⚠️ ATENÇÃO — a feature ${feature} tem claim VIVO de OUTRA sessão`
|
|
266
267
|
+ `\n pid ${other.pid}${other.path ? ` · ${other.path}` : ''} · visto há ${claimAge(other)}`
|
|
267
|
-
+ '\n Duas sessões na mesma feature já custaram um
|
|
268
|
+
+ '\n Duas sessões na mesma feature já custaram um relatório de fatia inteiro e um sub-agent'
|
|
268
269
|
+ '\n (o worktree sumiu debaixo dele). Confirme com a outra sessão ANTES de agir —'
|
|
269
270
|
+ '\n principalmente antes de `finish`, `archive` ou `delete`.'
|
|
270
271
|
+ `\n Se aquela sessão morreu: morph-spec claim ${feature} --release`;
|
|
@@ -56,28 +56,26 @@ SEP = f" {GRAY}│{R} "
|
|
|
56
56
|
|
|
57
57
|
# ── MORPH framework constants ────────────────────────────────────────────────
|
|
58
58
|
|
|
59
|
-
# Mirrors
|
|
60
|
-
# (scanning backwards) whose CANONICAL artifact exists wins. An
|
|
61
|
-
# is not enough.
|
|
59
|
+
# Mirrors derivePhaseFrom() in framework/hooks/shared/state-reader.js — the
|
|
60
|
+
# first phase (scanning backwards) whose CANONICAL artifact exists wins. An
|
|
61
|
+
# empty folder is not enough; a canonical ending in '/' is any .md under it.
|
|
62
62
|
PHASE_MAP = [
|
|
63
|
-
('4-review', 'review', '
|
|
64
|
-
('3-implement', 'implement', '
|
|
65
|
-
('2-plan', 'plan', 'spec.md'),
|
|
63
|
+
('4-review', 'review', 'verifier.md'),
|
|
64
|
+
('3-implement', 'implement', 'reports/'),
|
|
66
65
|
('1-design', 'uiux', 'design-system.md'),
|
|
67
|
-
('
|
|
66
|
+
('2-plan', 'plan', 'plan.md'),
|
|
68
67
|
]
|
|
69
|
-
PHASE_INDEX = {'
|
|
68
|
+
PHASE_INDEX = {'plan': 0, 'uiux': 1, 'implement': 2, 'review': 3}
|
|
70
69
|
PHASE_LABELS = {
|
|
71
|
-
'proposal': 'proposal',
|
|
72
|
-
'uiux': 'UI/UX',
|
|
73
70
|
'plan': 'planning',
|
|
71
|
+
'uiux': 'UI/UX',
|
|
74
72
|
'implement': 'implement',
|
|
75
73
|
'review': 'review',
|
|
76
74
|
}
|
|
77
|
-
PIPELINE_TOTAL =
|
|
75
|
+
PIPELINE_TOTAL = 4
|
|
78
76
|
|
|
79
77
|
# Gates that can block; `implement` has none. Mirrors feature.schema.json.
|
|
80
|
-
GATE_PHASES = ('
|
|
78
|
+
GATE_PHASES = ('plan', 'uiux', 'review')
|
|
81
79
|
|
|
82
80
|
|
|
83
81
|
# ── Auto-compact threshold ───────────────────────────────────────────────────
|
|
@@ -358,49 +356,65 @@ def get_worktree_label(payload, cwd):
|
|
|
358
356
|
return None
|
|
359
357
|
|
|
360
358
|
|
|
361
|
-
def _gate_implied_phase(gates):
|
|
362
|
-
"""Phase implied purely by approved gates (mirrors state-
|
|
359
|
+
def _gate_implied_phase(gates, design_started):
|
|
360
|
+
"""Phase implied purely by approved gates (mirrors state-reader.js)."""
|
|
363
361
|
def approved(gate):
|
|
364
362
|
return (gates.get(gate) or {}).get('approved') is True
|
|
365
363
|
if approved('review'):
|
|
366
364
|
return 'review'
|
|
367
365
|
if approved('plan'):
|
|
368
|
-
return 'implement'
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
366
|
+
return 'uiux' if design_started and not approved('uiux') else 'implement'
|
|
367
|
+
return 'plan'
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _has_artifact(folder, canonical):
|
|
371
|
+
if not canonical.endswith('/'):
|
|
372
|
+
return (folder / canonical).exists()
|
|
373
|
+
try:
|
|
374
|
+
return any(p.is_file() for p in (folder / canonical).glob('*.md'))
|
|
375
|
+
except OSError:
|
|
376
|
+
return False
|
|
372
377
|
|
|
373
378
|
|
|
374
379
|
def derive_phase(feature_dir, gates):
|
|
375
380
|
"""Phase from the filesystem, bumped by approved gates."""
|
|
376
|
-
artifact_phase = '
|
|
381
|
+
artifact_phase = 'plan'
|
|
377
382
|
for folder, phase, canonical in PHASE_MAP:
|
|
378
|
-
if (feature_dir / folder
|
|
383
|
+
if _has_artifact(feature_dir / folder, canonical):
|
|
379
384
|
artifact_phase = phase
|
|
380
385
|
break
|
|
381
|
-
implied = _gate_implied_phase(gates)
|
|
386
|
+
implied = _gate_implied_phase(gates, (feature_dir / '1-design').exists())
|
|
382
387
|
if PHASE_INDEX[implied] > PHASE_INDEX[artifact_phase]:
|
|
383
388
|
return implied
|
|
384
389
|
return artifact_phase
|
|
385
390
|
|
|
386
391
|
|
|
387
|
-
|
|
388
|
-
"""(done, total) from 2-plan/tasks.json — the declared source of truth.
|
|
392
|
+
PARTIAL_MARKER = re.compile(r'^[\s#>*_]*PARCIAL\b')
|
|
389
393
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
394
|
+
|
|
395
|
+
def count_slices(feature_dir):
|
|
396
|
+
"""(done, total) of the slices in 2-plan/checks.json.
|
|
397
|
+
|
|
398
|
+
Same rule as framework/hooks/shared/slice-progress.js: a slice is done when
|
|
399
|
+
3-implement/reports/{id}.md exists and its first line is not PARCIAL.
|
|
393
400
|
"""
|
|
394
401
|
try:
|
|
395
|
-
|
|
396
|
-
tasks = json.loads(raw).get('tasks')
|
|
397
|
-
if isinstance(tasks, list) and tasks:
|
|
398
|
-
done = sum(1 for task in tasks if task.get('status') in ('done', 'skipped'))
|
|
399
|
-
return done, len(tasks)
|
|
402
|
+
slices = json.loads((feature_dir / '2-plan' / 'checks.json').read_text(encoding='utf-8')).get('slices')
|
|
400
403
|
except Exception:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
+
return 0, 0
|
|
405
|
+
if not isinstance(slices, list):
|
|
406
|
+
return 0, 0
|
|
407
|
+
ids = [s.get('id') for s in slices if isinstance(s, dict) and isinstance(s.get('id'), str)]
|
|
408
|
+
done = 0
|
|
409
|
+
for slice_id in ids:
|
|
410
|
+
try:
|
|
411
|
+
text = (feature_dir / '3-implement' / 'reports' / f'{slice_id}.md').read_text(encoding='utf-8')
|
|
412
|
+
except Exception:
|
|
413
|
+
continue
|
|
414
|
+
first = next((line for line in text.splitlines() if line.strip()), '')
|
|
415
|
+
if not PARTIAL_MARKER.match(first):
|
|
416
|
+
done += 1
|
|
417
|
+
return done, len(ids)
|
|
404
418
|
|
|
405
419
|
|
|
406
420
|
def get_active_features(cwd):
|
|
@@ -428,15 +442,15 @@ def get_active_features(cwd):
|
|
|
428
442
|
feature_dir = path.parent
|
|
429
443
|
gates = data.get('approvalGates') or {}
|
|
430
444
|
phase = derive_phase(feature_dir, gates)
|
|
431
|
-
done, total =
|
|
445
|
+
done, total = count_slices(feature_dir)
|
|
432
446
|
pending = None
|
|
433
447
|
if phase in GATE_PHASES and (gates.get(phase) or {}).get('approved') is not True:
|
|
434
448
|
pending = phase
|
|
435
449
|
result.append({
|
|
436
450
|
'name': feature_dir.name,
|
|
437
451
|
'phase': phase,
|
|
438
|
-
'
|
|
439
|
-
'
|
|
452
|
+
'slices_done': done,
|
|
453
|
+
'slices_total': total,
|
|
440
454
|
'pending': pending,
|
|
441
455
|
})
|
|
442
456
|
return result
|
|
@@ -484,7 +498,7 @@ def render_morph_line(feature, hook_name, columns):
|
|
|
484
498
|
"""Line 0 — the active MORPH feature.
|
|
485
499
|
|
|
486
500
|
Goes through join_to_width like the status line: a feature name plus phase,
|
|
487
|
-
|
|
501
|
+
slice bar, gate and hook is the longest thing this script prints, and a
|
|
488
502
|
wrapping line 0 would defeat the whole point of the single-row layout.
|
|
489
503
|
"""
|
|
490
504
|
parts = [(0, f"{CYAN}{BOLD}► {feature['name']}{R}")]
|
|
@@ -497,12 +511,12 @@ def render_morph_line(feature, hook_name, columns):
|
|
|
497
511
|
else:
|
|
498
512
|
parts.append((1, f"{CYAN}{label} {position + 1}/{PIPELINE_TOTAL}{R}"))
|
|
499
513
|
|
|
500
|
-
if feature['
|
|
501
|
-
pct = feature['
|
|
514
|
+
if feature['slices_total'] > 0:
|
|
515
|
+
pct = feature['slices_done'] / feature['slices_total'] * 100
|
|
502
516
|
parts.append((
|
|
503
517
|
3,
|
|
504
518
|
f"{GREEN}{progress_bar(pct, 6)} "
|
|
505
|
-
f"{feature['
|
|
519
|
+
f"{feature['slices_done']}/{feature['slices_total']} fatias{R}"
|
|
506
520
|
))
|
|
507
521
|
|
|
508
522
|
if feature['pending']:
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Exported names/signatures are unchanged from the previous doc-based
|
|
20
20
|
* implementation on purpose — hooks outside this file's ownership
|
|
21
|
-
* (gate-guard.js,
|
|
21
|
+
* (gate-guard.js, enforce-phase-writes.js,
|
|
22
22
|
* loop-detect.js, protect-spec-files.js, validate-completion.js) call
|
|
23
23
|
* logHookActivity/logEvent and must keep working untouched. Only the
|
|
24
24
|
* on-disk format changed.
|
|
@@ -14,7 +14,7 @@ import { join } from 'path';
|
|
|
14
14
|
const DOTNET_BUILD_CMD = 'dotnet build --no-restore --nologo -v q';
|
|
15
15
|
const TSC_BUILD_CMD = 'npx tsc --noEmit';
|
|
16
16
|
|
|
17
|
-
function isDotnetStack(s) {
|
|
17
|
+
export function isDotnetStack(s) {
|
|
18
18
|
return s.includes('dotnet') || s.includes('aspnet');
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -70,16 +70,20 @@ export function getBuildCommand(stack, hasTsConfig = false) {
|
|
|
70
70
|
* `getBuildCommand()`'s existing single-stack behavior.
|
|
71
71
|
*
|
|
72
72
|
* @param {string} stack
|
|
73
|
-
*
|
|
73
|
+
* `backendTarget` names the solution/project to build (relative to the backend
|
|
74
|
+
* directory) when that directory holds none itself.
|
|
75
|
+
*
|
|
76
|
+
* @param {{ cwd: string, frontendPath?: string, backendPath?: string, backendTarget?: string }} opts
|
|
74
77
|
* @returns {Array<{ command: string, cwd: string }>}
|
|
75
78
|
*/
|
|
76
|
-
export function getBuildSteps(stack, { cwd, frontendPath, backendPath } = {}) {
|
|
79
|
+
export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget } = {}) {
|
|
77
80
|
if (!stack || !cwd) return [];
|
|
78
81
|
const s = stack.toLowerCase();
|
|
79
82
|
const steps = [];
|
|
80
83
|
|
|
81
84
|
if (isDotnetStack(s)) {
|
|
82
|
-
|
|
85
|
+
const command = backendTarget ? DOTNET_BUILD_CMD.replace(/^dotnet build/, `dotnet build "${backendTarget}"`) : DOTNET_BUILD_CMD;
|
|
86
|
+
steps.push({ command, cwd: backendPath ? join(cwd, backendPath) : cwd });
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
if (isFrontendStack(s)) {
|
|
@@ -129,11 +133,11 @@ export function parseBuildResult(exitCode, output) {
|
|
|
129
133
|
* @param {string} cwd - Project root directory
|
|
130
134
|
* @param {string} stack - Project stack string
|
|
131
135
|
* @param {number} [timeout=30000] - Timeout in ms
|
|
132
|
-
* @param {{ frontendPath?: string, backendPath?: string }} [paths] - Monorepo sub-paths, from config.json → project
|
|
136
|
+
* @param {{ frontendPath?: string, backendPath?: string, backendTarget?: string }} [paths] - Monorepo sub-paths, from config.json → project
|
|
133
137
|
* @returns {{ status: 'pass'|'fail'|'skip', output: string }}
|
|
134
138
|
*/
|
|
135
139
|
export function runBuildCheck(cwd, stack, timeout = 30000, paths = {}) {
|
|
136
|
-
const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath });
|
|
140
|
+
const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
|
|
137
141
|
if (steps.length === 0) return { status: 'skip', output: '' };
|
|
138
142
|
|
|
139
143
|
const failures = [];
|
|
@@ -117,3 +117,172 @@ export function exitCodeMasked(command) {
|
|
|
117
117
|
const last = segments[segments.length - 1] || '';
|
|
118
118
|
return (segments.length > 1 && REPORTER.test(last)) || /\|\|/.test(last.replace(/"[^"]*"|'[^']*'/g, ''));
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
const WRITE_REDIRECT = /^(?:[1&])?>>?\|?/;
|
|
122
|
+
|
|
123
|
+
function quotedTokens(text) {
|
|
124
|
+
const tokens = [];
|
|
125
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
126
|
+
let m;
|
|
127
|
+
while ((m = re.exec(text)) !== null) {
|
|
128
|
+
tokens.push({ text: m[1] ?? m[2] ?? m[3], quoted: m[3] === undefined, single: m[2] !== undefined });
|
|
129
|
+
}
|
|
130
|
+
return tokens;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const CHAIN = /^(?:&&|\|\||\||\|&)$/;
|
|
134
|
+
const ASSIGNMENT = /^([A-Za-z_]\w*)=(.*)$/;
|
|
135
|
+
const REDIRECT = /^(?:\d|&)?[<>]/;
|
|
136
|
+
const SCRIPT_RUNNER = /^(?:python[\d.]*|py|node|bun|deno)$/;
|
|
137
|
+
|
|
138
|
+
function simpleCommands(head) {
|
|
139
|
+
const commands = [];
|
|
140
|
+
for (const segment of topLevelSegments(head).segments) {
|
|
141
|
+
let current = [];
|
|
142
|
+
for (const token of quotedTokens(segment)) {
|
|
143
|
+
if (!token.quoted && CHAIN.test(token.text)) { commands.push(current); current = []; } else current.push(token);
|
|
144
|
+
}
|
|
145
|
+
commands.push(current);
|
|
146
|
+
}
|
|
147
|
+
return commands.filter((c) => c.length > 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function positionals(args, takesValue) {
|
|
151
|
+
const out = [];
|
|
152
|
+
const values = [];
|
|
153
|
+
for (let i = 0; i < args.length; i++) {
|
|
154
|
+
const { text, quoted } = args[i];
|
|
155
|
+
if (!quoted && REDIRECT.test(text)) { if (/[<>]$/.test(text)) i++; continue; }
|
|
156
|
+
if (!quoted && text.startsWith('-') && text !== '-') {
|
|
157
|
+
if (takesValue(text) && i + 1 < args.length) values.push(args[++i].text);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
out.push(text);
|
|
161
|
+
}
|
|
162
|
+
return { files: out, values };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sedTargets(args) {
|
|
166
|
+
const inPlace = args.some(({ text, quoted }) => !quoted && (/^-[A-Za-z]*i/.test(text) || text.startsWith('--in-place')));
|
|
167
|
+
if (!inPlace) return [];
|
|
168
|
+
const { files, values } = positionals(args, (o) => /^-[A-Za-z]*[ef]$/.test(o) || o === '--expression' || o === '--file');
|
|
169
|
+
return values.length > 0 ? files : files.slice(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function perlTargets(args) {
|
|
173
|
+
if (!args.some(({ text, quoted }) => !quoted && /^-[A-Za-z]*i/.test(text))) return [];
|
|
174
|
+
const { files, values } = positionals(args, (o) => /^-[A-Za-z.]*[eE]$/.test(o));
|
|
175
|
+
return values.length > 0 ? files : files.slice(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function copyTargets(args) {
|
|
179
|
+
if (args.some(({ text }) => text === '-t' || text.startsWith('--target-directory'))) return [];
|
|
180
|
+
const { files } = positionals(args, () => false);
|
|
181
|
+
if (files.length < 2) return [];
|
|
182
|
+
const dest = files[files.length - 1];
|
|
183
|
+
if (!dest.endsWith('/')) return [dest];
|
|
184
|
+
return files.slice(0, -1).map((src) => dest + src.replace(/^.*\//, ''));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const PY_OPEN_WRITE = /\bopen\(\s*([^,()]+?)\s*,\s*(?:mode\s*=\s*)?(['"])([^'"]*)\2/g;
|
|
188
|
+
const JS_WRITE = /\b(?:writeFileSync|writeFile|appendFileSync|appendFile)\(\s*([^,()]+?)\s*,/g;
|
|
189
|
+
const PY_PATH_WRITE = /\.write_(?:text|bytes)\(/;
|
|
190
|
+
const STRING_LITERAL = /(['"`])((?:(?!\1)[^\\\n]|\\.)*)\1/g;
|
|
191
|
+
const PATH_LITERAL = /^[\w@./-]*[\w-]\.[A-Za-z]{1,5}$/;
|
|
192
|
+
|
|
193
|
+
function literalOf(expr) {
|
|
194
|
+
const m = /^(['"`])([^'"`]*)\1$/.exec(expr.trim());
|
|
195
|
+
return m ? m[2] : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// A write call with a computed path (`open(p, "w")`) counts every path-shaped literal in the script.
|
|
199
|
+
function scriptWrites(script) {
|
|
200
|
+
const literals = [];
|
|
201
|
+
let computed = PY_PATH_WRITE.test(script);
|
|
202
|
+
for (const m of script.matchAll(PY_OPEN_WRITE)) {
|
|
203
|
+
if (!/[wax+]/.test(m[3])) continue;
|
|
204
|
+
const lit = literalOf(m[1]);
|
|
205
|
+
if (lit === null) computed = true; else literals.push(lit);
|
|
206
|
+
}
|
|
207
|
+
for (const m of script.matchAll(JS_WRITE)) {
|
|
208
|
+
const lit = literalOf(m[1]);
|
|
209
|
+
if (lit === null) computed = true; else literals.push(lit);
|
|
210
|
+
}
|
|
211
|
+
if (!computed) return literals;
|
|
212
|
+
const mentioned = [...script.matchAll(STRING_LITERAL)].map((m) => m[2]).filter((s) => PATH_LITERAL.test(s));
|
|
213
|
+
return [...new Set([...literals, ...mentioned])];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function runnerScript(args, body) {
|
|
217
|
+
for (let i = 0; i < args.length; i++) {
|
|
218
|
+
if (['-c', '-e', '-p', '--eval', '--print'].includes(args[i].text) && !args[i].quoted) return args[i + 1]?.text ?? null;
|
|
219
|
+
}
|
|
220
|
+
const { files } = positionals(args, () => false);
|
|
221
|
+
return (files.length === 0 || files[0] === '-') ? body : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function heredocBody(lines, at) {
|
|
225
|
+
if (at < 0) return null;
|
|
226
|
+
const delim = /<<-?\s*(['"]?)([\w.-]+)\1/.exec(lines[at])?.[2];
|
|
227
|
+
if (!delim) return null;
|
|
228
|
+
const end = lines.findIndex((line, i) => i > at && line.trim() === delim);
|
|
229
|
+
return lines.slice(at + 1, end < 0 ? undefined : end).join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function commandWriteTargets(head, body) {
|
|
233
|
+
const vars = new Map();
|
|
234
|
+
const expand = ({ text, single }) => (single ? text : text.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (all, name) => vars.get(name) ?? all));
|
|
235
|
+
const targets = [];
|
|
236
|
+
for (const tokens of simpleCommands(head)) {
|
|
237
|
+
let i = 0;
|
|
238
|
+
for (; i < tokens.length && !tokens[i].quoted && ASSIGNMENT.test(tokens[i].text); i++) {
|
|
239
|
+
const [, name, value] = ASSIGNMENT.exec(tokens[i].text);
|
|
240
|
+
vars.set(name, value);
|
|
241
|
+
}
|
|
242
|
+
if (i >= tokens.length) continue;
|
|
243
|
+
const word = tokens[i].text.replace(/^.*[\\/]/, '').replace(/\.exe$/i, '');
|
|
244
|
+
const args = tokens.slice(i + 1).map((t) => ({ ...t, text: expand(t) }));
|
|
245
|
+
if (word === 'sed') targets.push(...sedTargets(args));
|
|
246
|
+
else if (word === 'perl') targets.push(...perlTargets(args));
|
|
247
|
+
else if (word === 'cp' || word === 'mv') targets.push(...copyTargets(args));
|
|
248
|
+
else if (SCRIPT_RUNNER.test(word)) {
|
|
249
|
+
const script = runnerScript(args, body);
|
|
250
|
+
if (script) targets.push(...scriptWrites(script));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return targets;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Files a command writes: stdout redirection (`>`, `>>`, `1>`, `&>`), `tee`, `sed -i`,
|
|
258
|
+
* `perl -i`, the `cp`/`mv` destination, and a python/node `-c`/`-e`/heredoc script's writes.
|
|
259
|
+
* The redirect scan stops at the line that opens a heredoc: the body is data, and an
|
|
260
|
+
* apostrophe in it would derail the quote-aware split. Quoted operators are text.
|
|
261
|
+
* @param {string} command
|
|
262
|
+
* @returns {string[]}
|
|
263
|
+
*/
|
|
264
|
+
export function writeTargets(command) {
|
|
265
|
+
const lines = String(command || '').split('\n');
|
|
266
|
+
const heredocAt = lines.findIndex((line) => line.includes('<<'));
|
|
267
|
+
const head = (heredocAt >= 0 ? lines.slice(0, heredocAt + 1) : lines).join('\n');
|
|
268
|
+
const tokens = quotedTokens(head);
|
|
269
|
+
|
|
270
|
+
const targets = [];
|
|
271
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
272
|
+
const { text, quoted } = tokens[i];
|
|
273
|
+
if (quoted) continue;
|
|
274
|
+
const op = text.match(WRITE_REDIRECT);
|
|
275
|
+
if (op) {
|
|
276
|
+
const rest = text.slice(op[0].length);
|
|
277
|
+
const target = rest || tokens[i + 1]?.text;
|
|
278
|
+
if (target && !target.startsWith('&')) targets.push(target);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (text.replace(/^.*[\/]/, '') === 'tee') {
|
|
282
|
+
for (let j = i + 1; j < tokens.length && !/^[;&|]/.test(tokens[j].text); j++) {
|
|
283
|
+
if (!tokens[j].text.startsWith('-')) targets.push(tokens[j].text);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return [...new Set([...targets, ...commandWriteTargets(head, heredocBody(lines, heredocAt))])];
|
|
288
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The comment shapes the `architecture-code-comments` standard forbids in
|
|
3
3
|
* every case, judged only on the comments an edit ADDED. Whether a short
|
|
4
4
|
* comment restates the code is not decidable here; that stays with the
|
|
5
|
-
* Gate 3
|
|
5
|
+
* Gate 3 reviewer and the `code-budget` density.
|
|
6
6
|
*
|
|
7
7
|
* @module comment-rules
|
|
8
8
|
*/
|
|
@@ -10,42 +10,29 @@
|
|
|
10
10
|
|
|
11
11
|
import { stripComments } from './comment-stripper.js';
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Id de cenário (`C{n}`) e de fatia (`F{n}`) do plano. A mesma grafia também é
|
|
15
|
+
* tecla de função, métrica, célula de planilha ou o modelo C-quatro: esses
|
|
16
|
+
* vizinhos não contam (casos em comment-scanner.test.js).
|
|
17
|
+
*/
|
|
18
|
+
const PLAN_ID = new RegExp(String.raw`(?<!\b(?:key|tecla|press|aperte|cell|c[eé]lula|column|coluna)\s+)(?<![\w.#$/-])[CF]\d{1,2}(?![\w.(-]|\s+(?:key|tecla|score|model|modelo|diagram|diagrama)\b)`, 'i');
|
|
19
|
+
|
|
14
20
|
const PROCESS_CITATION = [
|
|
15
21
|
/\bUC[-\s]?\d+/,
|
|
16
22
|
/\bADR[-\s]?\d+/,
|
|
17
23
|
/\bFR[-\s]?\d+/,
|
|
18
24
|
/\bgate\s?[1-3]\b/i,
|
|
19
|
-
/\
|
|
25
|
+
/\bplan\.md\b/i,
|
|
26
|
+
/\bchecks\.json\b/i,
|
|
20
27
|
/\bmockups?\b/i,
|
|
21
28
|
];
|
|
22
29
|
|
|
23
|
-
/**
|
|
24
|
-
* `T{n}` também é nome de parâmetro genérico. Não casa depois de `template`/
|
|
25
|
-
* `typename`/`generic`, nem colado a letra ou dígito (a hora ISO `24T10`).
|
|
26
|
-
*/
|
|
27
|
-
const TASK_ID = /(?<!\b(?:[Tt]emplate|[Tt]ypename|[Gg]eneric)\s+)(?<![\w"])T\d{1,3}\b(?!")/;
|
|
28
|
-
|
|
29
|
-
/** Lista de tipos `<…>` e `{…}` (`Func<T1, T2>`, `cref="Func{T1,T2}"`, XML doc), de dentro para fora. */
|
|
30
|
-
const TYPE_LIST = /<[^<>]*>|\{[^{}]*\}/g;
|
|
31
|
-
|
|
32
|
-
function withoutTypeLists(text) {
|
|
33
|
-
let previous;
|
|
34
|
-
let current = text;
|
|
35
|
-
do {
|
|
36
|
-
previous = current;
|
|
37
|
-
current = current.replace(TYPE_LIST, ' ');
|
|
38
|
-
} while (current !== previous);
|
|
39
|
-
return current;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
30
|
/**
|
|
43
31
|
* @param {string} commentText
|
|
44
32
|
* @returns {boolean} o comentário usa o vocabulário de processo acima
|
|
45
33
|
*/
|
|
46
34
|
export function citesProcess(commentText) {
|
|
47
|
-
return PROCESS_CITATION.some((re) => re.test(commentText))
|
|
48
|
-
|| TASK_ID.test(withoutTypeLists(commentText));
|
|
35
|
+
return PROCESS_CITATION.some((re) => re.test(commentText)) || PLAN_ID.test(commentText);
|
|
49
36
|
}
|
|
50
37
|
|
|
51
38
|
/**
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - `registry-audit.js` — precisa ler **JSONC**, a forma que o exemplar
|
|
11
11
|
* canônico `model-registry.v2.json` usa e que o leitor C# do kit aceita de
|
|
12
12
|
* propósito (`JsonCommentHandling.Skip`, `AllowTrailingCommas`);
|
|
13
|
-
* - `dotnet-
|
|
13
|
+
* - `mutation/dotnet-introspection.js` — lê o `global.json` por `jsoncToJson`;
|
|
14
14
|
* - `comment-scanner.js` (ao lado; lido pelo `code-budget` e pelo hook
|
|
15
15
|
* `comment-guard`) — acha as linhas de
|
|
16
16
|
* comentário de `.cs`/`.ts`/`.js`, e é o único que liga a opção `js`.
|