guardian-framework 0.1.105 → 0.1.110
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/README.md +11 -2
- package/dist/cli.js +232 -16
- package/dist/exports.js +232 -16
- package/package.json +1 -1
- package/templates/languages/rust-patterns.md +5 -2
- package/templates/pi/context/checklists.md +15 -1
- package/templates/pi/domain/exploration.md +9 -0
- package/templates/pi/domain/ubiquitous-language.md +12 -0
- package/templates/pi/extensions/architect-lib/generators.ts +158 -7
- package/templates/pi/extensions/architect-lib/helpers.ts +49 -0
- package/templates/pi/extensions/architect-lib/types.ts +44 -2
- package/templates/pi/extensions/architect.ts +102 -42
- package/templates/pi/extensions/ask-user-question.ts +2 -2
- package/templates/pi/extensions/bash-guard.ts +2 -2
- package/templates/pi/extensions/config-reload.ts +12 -5
- package/templates/pi/extensions/coordinator.ts +2 -2
- package/templates/pi/extensions/curator.ts +1 -1
- package/templates/pi/extensions/domain-explorer.ts +50 -3
- package/templates/pi/extensions/filechanges.ts +2 -2
- package/templates/pi/extensions/goal-loop.ts +3 -3
- package/templates/pi/extensions/hooks.ts +1 -1
- package/templates/pi/extensions/pi-env.d.ts +96 -0
- package/templates/pi/extensions/pipeline.ts +1 -1
- package/templates/pi/extensions/project-scaffolder.ts +1 -1
- package/templates/pi/extensions/read-only-mode.ts +5 -1
- package/templates/pi/extensions/tsconfig.json +9 -2
- package/templates/pi/preflight_report.json +1 -1
- package/templates/pi/prompts/ci-blueprint.md +2 -2
- package/templates/pi/scripts/check_model_judgment.sh +28 -0
- package/templates/pi/scripts/ci/check_judgment_contracts.sh +93 -0
- package/templates/pi/scripts/ci/run_preflight.sh +3 -3
- package/templates/pi/scripts/ci/run_stage.sh +1 -1
- package/templates/pi/scripts/ci/stage_lint.sh +1 -1
- package/templates/pi/scripts/coverage.sh +58 -0
- package/templates/pi/scripts/install_coverage_tools.sh +43 -0
- package/templates/pi/scripts/languages/go/check_model_judgment.sh +70 -0
- package/templates/pi/scripts/languages/go/check_runtime_reachability.sh +34 -0
- package/templates/pi/scripts/languages/go/coverage.sh +78 -0
- package/templates/pi/scripts/languages/go/install_coverage_tools.sh +17 -0
- package/templates/pi/scripts/languages/java/check_model_judgment.sh +142 -0
- package/templates/pi/scripts/languages/java/check_runtime_reachability.sh +31 -0
- package/templates/pi/scripts/languages/java/coverage.sh +103 -0
- package/templates/pi/scripts/languages/java/install_coverage_tools.sh +40 -0
- package/templates/pi/scripts/languages/java/validate-architecture.sh +20 -0
- package/templates/pi/scripts/languages/python/check_model_judgment.sh +67 -0
- package/templates/pi/scripts/languages/python/check_runtime_reachability.sh +34 -0
- package/templates/pi/scripts/languages/python/coverage.sh +88 -0
- package/templates/pi/scripts/languages/python/install_coverage_tools.sh +22 -0
- package/templates/pi/scripts/languages/rust/check_model_judgment.sh +61 -0
- package/templates/pi/scripts/languages/rust/check_runtime_reachability.sh +40 -0
- package/templates/pi/scripts/languages/rust/coverage.sh +79 -0
- package/templates/pi/scripts/languages/rust/install_coverage_tools.sh +20 -0
- package/templates/pi/scripts/languages/rust/validate-tests.sh +9 -6
- package/templates/pi/scripts/languages/typescript/check_model_judgment.sh +81 -0
- package/templates/pi/scripts/languages/typescript/check_runtime_reachability.sh +41 -0
- package/templates/pi/scripts/languages/typescript/coverage.sh +154 -0
- package/templates/pi/scripts/languages/typescript/install_coverage_tools.sh +41 -0
- package/templates/pi/scripts/languages/typescript/validate-canonical.sh +6 -2
- package/templates/pi/scripts/languages/typescript/validate-ci.sh +2 -2
- package/templates/pi/scripts/validate-architecture-readiness.sh +29 -0
- package/templates/pi/scripts/validate-exploration-depth.sh +112 -0
- package/templates/pi/scripts/validate-runtime-wiring.sh +35 -0
- package/templates/pi/skills/agents/architecture-validator.md +28 -0
- package/templates/pi/skills/agents/test-validator.md +1 -1
- package/templates/pi/skills/go-enterprise-codegen.md +46 -0
- package/templates/pi/skills/java-spring-enterprise-codegen.md +50 -0
- package/templates/pi/skills/python-enterprise-codegen.md +46 -0
- package/templates/pi/skills/rust-enterprise-codegen.md +46 -0
- package/templates/pi/skills/typescript-enterprise-codegen.md +46 -0
|
@@ -197,6 +197,26 @@ export function generateIssueMarkdown(
|
|
|
197
197
|
const title = buildIssueTitle(comp, moduleId);
|
|
198
198
|
const inScope = buildInScope(comp, compAcs, implSteps, moduleId);
|
|
199
199
|
|
|
200
|
+
// ADR-008: append compact modeling directives for aggregate/root components.
|
|
201
|
+
const isTactical = comp.modelingDepth === "tactical" || comp.aggregateRoot;
|
|
202
|
+
if (isTactical && (comp.invariants?.length || comp.events)) {
|
|
203
|
+
if (comp.invariants?.length) {
|
|
204
|
+
inScope.push("Honor the frozen aggregate invariants: " + comp.invariants.join("; "));
|
|
205
|
+
}
|
|
206
|
+
if (comp.events) {
|
|
207
|
+
const ev = [
|
|
208
|
+
...(comp.events.publishes?.length ? ["publishes " + comp.events.publishes.join(", ")] : []),
|
|
209
|
+
...(comp.events.subscribes?.length ? ["subscribes " + comp.events.subscribes.join(", ")] : []),
|
|
210
|
+
].join(", ");
|
|
211
|
+
if (ev) inScope.push("Domain events: " + ev);
|
|
212
|
+
}
|
|
213
|
+
if (comp.aggregateRoot) {
|
|
214
|
+
inScope.push(
|
|
215
|
+
"Implement " + comp.name + " as the aggregate root — no setters; state changes via behavior methods; repository interface owned by the domain layer",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
200
220
|
// Language-aware test paths
|
|
201
221
|
const lang = detectLanguage(cwd);
|
|
202
222
|
const ext = implExtension(lang);
|
|
@@ -373,6 +393,30 @@ export function generateContractFreezeMarkdown(
|
|
|
373
393
|
const implSteps = parseImplSequence(doc);
|
|
374
394
|
const allAcs = parseAcTable(doc);
|
|
375
395
|
|
|
396
|
+
// ADR-008 modeling contract for components carrying depth metadata.
|
|
397
|
+
const modelingParts = slice.nextLogicalSlice
|
|
398
|
+
.filter((c: ModuleComponent) => c.modelingDepth === "tactical" || c.aggregateRoot || c.invariants?.length || c.events)
|
|
399
|
+
.map((c: ModuleComponent) => {
|
|
400
|
+
const lines: string[] = [`### ${c.name}`];
|
|
401
|
+
if (c.aggregateRoot) {
|
|
402
|
+
lines.push("- Aggregate root — all state changes flow through behavior methods (no setters)");
|
|
403
|
+
lines.push("- Repository interface is owned by the domain layer (interface-ownership, ADR-008)");
|
|
404
|
+
lines.push("- One aggregate per transaction; reference other aggregates by identity");
|
|
405
|
+
}
|
|
406
|
+
if (c.invariants?.length) {
|
|
407
|
+
lines.push("- Frozen invariants (become pre-generated failing tests):");
|
|
408
|
+
for (const inv of c.invariants) lines.push(` - ${inv}`);
|
|
409
|
+
}
|
|
410
|
+
if (c.events) {
|
|
411
|
+
if (c.events.publishes?.length) lines.push("- Publishes: " + c.events.publishes.join(", "));
|
|
412
|
+
if (c.events.subscribes?.length) lines.push("- Subscribes: " + c.events.subscribes.join(", "));
|
|
413
|
+
}
|
|
414
|
+
return lines.join("\n");
|
|
415
|
+
});
|
|
416
|
+
const modelingContract = modelingParts.length > 0
|
|
417
|
+
? "## Modeling Contract (ADR-008)\n\n" + modelingParts.join("\n\n")
|
|
418
|
+
: "";
|
|
419
|
+
|
|
376
420
|
return `---
|
|
377
421
|
guardian_issue:
|
|
378
422
|
id: "ISSUE-CONTRACT-FREEZE"
|
|
@@ -481,6 +525,8 @@ ${implSteps.length > 0
|
|
|
481
525
|
? `## Full Implementation Sequence (for reference)\n\n${implSteps.map((s) => '- ' + s).join("\n")}`
|
|
482
526
|
: ""}
|
|
483
527
|
|
|
528
|
+
${modelingContract}
|
|
529
|
+
|
|
484
530
|
## Implementation
|
|
485
531
|
|
|
486
532
|
> **Agent:** Create interface-only files. No implementation. Use Clean Architecture layers:
|
|
@@ -552,7 +598,6 @@ guardian_issue:
|
|
|
552
598
|
|
|
553
599
|
file_changes:
|
|
554
600
|
- "create: .pi/scripts/ci/check_${moduleId}_contracts.sh"
|
|
555
|
-
- "create: .pi/scripts/ci/check_${moduleId}_coverage.sh"
|
|
556
601
|
- "modify: .pi/scripts/ci/run_hardening_stages.sh"
|
|
557
602
|
---
|
|
558
603
|
|
|
@@ -572,10 +617,15 @@ automatic — no human review needed for routine checks.
|
|
|
572
617
|
- Verifies all interface methods are implemented
|
|
573
618
|
- Reports violations with file:line references
|
|
574
619
|
|
|
575
|
-
### Coverage Threshold Check
|
|
576
|
-
-
|
|
577
|
-
|
|
578
|
-
|
|
620
|
+
### Coverage Threshold Check (real project coverage)
|
|
621
|
+
- Coverage is enforced with the PROJECT'S REAL coverage tooling and gate
|
|
622
|
+
(e.g. \`cargo llvm-cov\` via a \`.pi/scripts/coverage.sh --gate\`, \`pytest
|
|
623
|
+
--cov\`, or \`c8\`/\`nyc\` for TS — whatever the project's CI coverage job
|
|
624
|
+
uses); assert the aggregate threshold the project actually enforces
|
|
625
|
+
- Do NOT create a per-module \`*_coverage.sh\` script — grep-based module
|
|
626
|
+
coverage scripts are coverage-theater (count test functions, never a
|
|
627
|
+
percentage); the real coverage tool's artifact (lcov/cobertura) provides
|
|
628
|
+
the per-file breakdown instead
|
|
579
629
|
|
|
580
630
|
### CI Integration
|
|
581
631
|
Each check becomes a CI stage in the hardening pipeline — it runs automatically
|
|
@@ -586,7 +636,6 @@ on every PR. No LLM cost. No human review. Just pass or fail.
|
|
|
586
636
|
| Script | Purpose | Location |
|
|
587
637
|
|--------|---------|----------|
|
|
588
638
|
| check_${moduleId}_contracts.sh | Validate contract implementation | .pi/scripts/ci/ |
|
|
589
|
-
| check_${moduleId}_coverage.sh | Enforce coverage thresholds | .pi/scripts/ci/ |
|
|
590
639
|
| stage_${moduleId}_proofing.sh | CI stage wrapper | .pi/scripts/ci/ |
|
|
591
640
|
|
|
592
641
|
## CI Pipeline Update
|
|
@@ -604,7 +653,7 @@ run_stage "11" "${moduleId}_proofing" \\
|
|
|
604
653
|
| # | Criterion | Script |
|
|
605
654
|
|---|-----------|--------|
|
|
606
655
|
| 1 | All interfaces have implementations | check_contracts.sh |
|
|
607
|
-
| 2 | Coverage
|
|
656
|
+
| 2 | Coverage meets the project's real coverage gate (project coverage tool + threshold in CI) | project coverage job (e.g. .pi/scripts/coverage.sh --gate) |
|
|
608
657
|
| 3 | CI runs checks on every PR | run_hardening_stages.sh |
|
|
609
658
|
| 4 | All scripts exit 0 on pass, 1 on fail | self-validating |
|
|
610
659
|
|
|
@@ -634,6 +683,108 @@ run_stage "11" "${moduleId}_proofing" \\
|
|
|
634
683
|
|
|
635
684
|
// ── Architecture Readiness Generator (expanded) ──
|
|
636
685
|
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* ADR-009: Runtime Integration & Legacy Migration issue generator.
|
|
689
|
+
* Modules whose doc documents runtime integration get a mandatory epic item
|
|
690
|
+
* (after implementation, before proofing) so "implemented but never wired"
|
|
691
|
+
* can no longer be deferred via an honest note.
|
|
692
|
+
*/
|
|
693
|
+
export function moduleHasRuntimeIntegration(cwd: string, moduleId: string): boolean {
|
|
694
|
+
const doc = readModuleDoc(cwd, moduleId);
|
|
695
|
+
if (!doc) return false;
|
|
696
|
+
return /^#{2,3}\s+(Integration|Runtime|Dispatch|Wiring|.*Dispatch.*)/im.test(doc);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function generateRuntimeIntegrationMarkdown(
|
|
700
|
+
slice: ArchitectureSlice,
|
|
701
|
+
epicName: string,
|
|
702
|
+
cwd?: string,
|
|
703
|
+
): string {
|
|
704
|
+
const moduleId = slice.module.replace(/^module-/, "");
|
|
705
|
+
const doc = cwd ? readModuleDoc(cwd, slice.module) : "";
|
|
706
|
+
// Pull the documented integration seam(s) as guidance for the implementer.
|
|
707
|
+
const seam = (() => {
|
|
708
|
+
for (const heading of ["Integration with Existing Modules", "Runtime Integration", "Integration"]) {
|
|
709
|
+
const body = extractSection(doc, heading);
|
|
710
|
+
if (body && body.trim().length > 0) return `## ${heading}\n\n${body.trim().slice(0, 1800)}`;
|
|
711
|
+
}
|
|
712
|
+
return "";
|
|
713
|
+
})();
|
|
714
|
+
|
|
715
|
+
return `---
|
|
716
|
+
guardian_issue:
|
|
717
|
+
id: "ISSUE-RUNTIME-INTEGRATION"
|
|
718
|
+
epic: "${epicName}"
|
|
719
|
+
component: "Runtime Integration & Legacy Migration"
|
|
720
|
+
module: "${slice.module}"
|
|
721
|
+
status: planned
|
|
722
|
+
priority: high
|
|
723
|
+
dependencies: []
|
|
724
|
+
|
|
725
|
+
in_scope:
|
|
726
|
+
- Wire the new service(s) into the REAL runtime path — every choke point the module doc
|
|
727
|
+
documents (primary dispatch loop PLUS parallel / resume / hook variants; not just one seam)
|
|
728
|
+
- Remove or delegate the legacy path the module replaces (legacy symbol deleted or calling
|
|
729
|
+
the new service); honor any declared migration rule (e.g. GAP-N)
|
|
730
|
+
- Flip the feature ADR from Proposed to Accepted once wired
|
|
731
|
+
- Add an end-to-end test through the PUBLIC API that exercises the new path
|
|
732
|
+
|
|
733
|
+
out_of_scope:
|
|
734
|
+
- New feature behavior beyond wiring
|
|
735
|
+
- Schema/data migrations (unless the module doc's migration rule requires them)
|
|
736
|
+
|
|
737
|
+
canonical_references:
|
|
738
|
+
- module: ".pi/architecture/modules/${slice.module}.md"
|
|
739
|
+
- adr: ".pi/architecture/decisions/ADR-009-runtime-wiring-multilang-judgment.md"
|
|
740
|
+
|
|
741
|
+
acceptance_criteria:
|
|
742
|
+
- "New service reachable from the runtime entry path (production, non-test caller)"
|
|
743
|
+
- "Legacy path removed or delegating; migration rule honored"
|
|
744
|
+
- "Feature ADR Accepted"
|
|
745
|
+
- "E2E through the public API exercises the new path"
|
|
746
|
+
- "validate-runtime-wiring.sh passes for the module (no dead service)"
|
|
747
|
+
|
|
748
|
+
validators:
|
|
749
|
+
- ci
|
|
750
|
+
- tests
|
|
751
|
+
- architecture
|
|
752
|
+
- operations
|
|
753
|
+
- canonical
|
|
754
|
+
|
|
755
|
+
implementation_notes: |
|
|
756
|
+
Run .pi/scripts/validate-runtime-wiring.sh before and after. If the new service
|
|
757
|
+
is only referenced by its own tests, it is NOT integrated. Converting the
|
|
758
|
+
documented dispatch choke point(s) is the acceptance slice — do not close this
|
|
759
|
+
issue with an honest deferral note.
|
|
760
|
+
|
|
761
|
+
file_changes:
|
|
762
|
+
- "modify: runtime dispatch / composition root (per module doc integration section)"
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
# Runtime Integration & Legacy Migration: ${slice.module}
|
|
766
|
+
|
|
767
|
+
## Intent
|
|
768
|
+
|
|
769
|
+
The implementation issues above delivered tested components. This issue makes them
|
|
770
|
+
**actually run**: wire them into the runtime dispatch path, retire the legacy path,
|
|
771
|
+
and prove it with an end-to-end test — per ADR-009 (implemented-but-unwired is the
|
|
772
|
+
gap this closes).
|
|
773
|
+
|
|
774
|
+
${seam || "> No explicit integration section found in the module doc — trace the runtime entry path (dispatch/composition root) yourself."}
|
|
775
|
+
|
|
776
|
+
## Acceptance Criteria
|
|
777
|
+
|
|
778
|
+
| # | Criterion | How to Verify |
|
|
779
|
+
|---|-----------|---------------|
|
|
780
|
+
| 1 | Service reachable from runtime entry path | grep production (non-test) caller outside the module |
|
|
781
|
+
| 2 | Legacy path removed or delegating | legacy symbol deleted / delegates to new service |
|
|
782
|
+
| 3 | Feature ADR Accepted | ADR Status not Proposed |
|
|
783
|
+
| 4 | E2E exercises the new path through the public API | integration/e2e test |
|
|
784
|
+
| 5 | validate-runtime-wiring.sh passes | script exit 0 |
|
|
785
|
+
`;
|
|
786
|
+
}
|
|
787
|
+
|
|
637
788
|
export function generateArchitectureReadinessMarkdown(
|
|
638
789
|
slice: ArchitectureSlice,
|
|
639
790
|
epicName: string,
|
|
@@ -353,6 +353,11 @@ export function parseModuleFile(filePath: string): ModuleComponent[] {
|
|
|
353
353
|
let currentStatus = "";
|
|
354
354
|
let currentDesc = "";
|
|
355
355
|
let currentDeps: string[] = [];
|
|
356
|
+
let currentDepth = undefined as ModuleComponent["modelingDepth"];
|
|
357
|
+
let currentAggregateRoot = false;
|
|
358
|
+
let currentInvariants: string[] = [];
|
|
359
|
+
let currentPublishes: string[] = [];
|
|
360
|
+
let currentSubscribes: string[] = [];
|
|
356
361
|
|
|
357
362
|
function saveCurrent() {
|
|
358
363
|
if (currentName) {
|
|
@@ -364,6 +369,17 @@ export function parseModuleFile(filePath: string): ModuleComponent[] {
|
|
|
364
369
|
status: status as ModuleComponent["status"],
|
|
365
370
|
description: desc.trim(),
|
|
366
371
|
dependencies: currentDeps.length > 0 ? currentDeps : ["none"],
|
|
372
|
+
...(currentDepth ? { modelingDepth: currentDepth } : {}),
|
|
373
|
+
...(currentAggregateRoot ? { aggregateRoot: true } : {}),
|
|
374
|
+
...(currentInvariants.length > 0 ? { invariants: currentInvariants } : {}),
|
|
375
|
+
...(currentPublishes.length > 0 || currentSubscribes.length > 0
|
|
376
|
+
? {
|
|
377
|
+
events: {
|
|
378
|
+
publishes: currentPublishes,
|
|
379
|
+
subscribes: currentSubscribes,
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
: {}),
|
|
367
383
|
});
|
|
368
384
|
}
|
|
369
385
|
}
|
|
@@ -384,6 +400,11 @@ export function parseModuleFile(filePath: string): ModuleComponent[] {
|
|
|
384
400
|
currentStatus = "";
|
|
385
401
|
currentDesc = "";
|
|
386
402
|
currentDeps = [];
|
|
403
|
+
currentDepth = undefined;
|
|
404
|
+
currentAggregateRoot = false;
|
|
405
|
+
currentInvariants = [];
|
|
406
|
+
currentPublishes = [];
|
|
407
|
+
currentSubscribes = [];
|
|
387
408
|
inComponentSection = false;
|
|
388
409
|
inDetailsSection = false;
|
|
389
410
|
continue;
|
|
@@ -401,6 +422,11 @@ export function parseModuleFile(filePath: string): ModuleComponent[] {
|
|
|
401
422
|
currentStatus = "";
|
|
402
423
|
currentDesc = "";
|
|
403
424
|
currentDeps = [];
|
|
425
|
+
currentDepth = undefined;
|
|
426
|
+
currentAggregateRoot = false;
|
|
427
|
+
currentInvariants = [];
|
|
428
|
+
currentPublishes = [];
|
|
429
|
+
currentSubscribes = [];
|
|
404
430
|
continue;
|
|
405
431
|
}
|
|
406
432
|
|
|
@@ -413,6 +439,29 @@ export function parseModuleFile(filePath: string): ModuleComponent[] {
|
|
|
413
439
|
if (depsStr && depsStr !== "none" && depsStr !== "[TODO") {
|
|
414
440
|
currentDeps = depsStr.split(",").map((d) => d.trim()).filter(Boolean);
|
|
415
441
|
}
|
|
442
|
+
} else if (trimmed.startsWith("modelingDepth:")) {
|
|
443
|
+
const v = trimmed.replace("modelingDepth:", "").trim();
|
|
444
|
+
if (v === "tactical" || v === "service" || v === "adapter") currentDepth = v;
|
|
445
|
+
} else if (trimmed.startsWith("aggregateRoot:")) {
|
|
446
|
+
currentAggregateRoot = trimmed.replace("aggregateRoot:", "").trim() === "true";
|
|
447
|
+
} else if (trimmed.startsWith("invariants:")) {
|
|
448
|
+
currentInvariants = trimmed
|
|
449
|
+
.replace("invariants:", "")
|
|
450
|
+
.split(";")
|
|
451
|
+
.map((x) => x.trim())
|
|
452
|
+
.filter(Boolean);
|
|
453
|
+
} else if (trimmed.startsWith("publishes:")) {
|
|
454
|
+
currentPublishes = trimmed
|
|
455
|
+
.replace("publishes:", "")
|
|
456
|
+
.split(",")
|
|
457
|
+
.map((x) => x.trim())
|
|
458
|
+
.filter(Boolean);
|
|
459
|
+
} else if (trimmed.startsWith("subscribes:")) {
|
|
460
|
+
currentSubscribes = trimmed
|
|
461
|
+
.replace("subscribes:", "")
|
|
462
|
+
.split(",")
|
|
463
|
+
.map((x) => x.trim())
|
|
464
|
+
.filter(Boolean);
|
|
416
465
|
} else if (trimmed.startsWith("**Purpose:**")) {
|
|
417
466
|
currentDesc = trimmed.replace(/\*\*Purpose:\*\*\s*/, "").trim();
|
|
418
467
|
} else if (!currentDesc && trimmed.length > 10 && !trimmed.startsWith("#") && !trimmed.startsWith("-") && !trimmed.startsWith("|") && !trimmed.startsWith(">") && !trimmed.startsWith("```")) {
|
|
@@ -55,12 +55,16 @@ export type ExtensionContext = {
|
|
|
55
55
|
notify(message: string, level?: string): void;
|
|
56
56
|
setStatus(key: string, message: string | null): void;
|
|
57
57
|
confirm(title: string, message: string): Promise<boolean>;
|
|
58
|
+
theme?: unknown;
|
|
58
59
|
};
|
|
59
60
|
tools: { execute(name: string, params: Record<string, unknown>): Promise<unknown> };
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
export type ExtensionAPI = {
|
|
63
|
-
on(
|
|
64
|
+
on(
|
|
65
|
+
event: string,
|
|
66
|
+
handler: (event: unknown, ctx: ExtensionContext) => unknown | Promise<unknown>,
|
|
67
|
+
): void;
|
|
64
68
|
registerTool(options: {
|
|
65
69
|
name: string;
|
|
66
70
|
label: string;
|
|
@@ -70,7 +74,7 @@ export type ExtensionAPI = {
|
|
|
70
74
|
toolCallId: string,
|
|
71
75
|
params: Record<string, unknown>,
|
|
72
76
|
signal: AbortSignal,
|
|
73
|
-
onUpdate: (update: { type
|
|
77
|
+
onUpdate: (update: { type?: string; message?: string; content?: unknown }) => void,
|
|
74
78
|
ctx: ExtensionContext,
|
|
75
79
|
): unknown | Promise<unknown>;
|
|
76
80
|
}): void;
|
|
@@ -96,6 +100,14 @@ export type ModuleComponent = {
|
|
|
96
100
|
status: "planned" | "in-progress" | "implemented" | "deprecated";
|
|
97
101
|
description: string;
|
|
98
102
|
dependencies: string[];
|
|
103
|
+
/** ADR-008 modeling depth for this component's module (from exploration) */
|
|
104
|
+
modelingDepth?: "tactical" | "service" | "adapter";
|
|
105
|
+
/** True when this component is an aggregate root — slice selection keeps it atomic */
|
|
106
|
+
aggregateRoot?: boolean;
|
|
107
|
+
/** Aggregate invariants (ADR-008) — become frozen contract + TDD tests */
|
|
108
|
+
invariants?: string[];
|
|
109
|
+
/** Domain events this aggregate publishes/subscribes to (ADR-008) */
|
|
110
|
+
events?: { publishes: string[]; subscribes: string[] };
|
|
99
111
|
};
|
|
100
112
|
|
|
101
113
|
export type ArchitectureSlice = {
|
|
@@ -104,6 +116,36 @@ export type ArchitectureSlice = {
|
|
|
104
116
|
nextLogicalSlice: ModuleComponent[];
|
|
105
117
|
};
|
|
106
118
|
|
|
119
|
+
|
|
120
|
+
/** A module row inside a roadmap phase (implementation-roadmap.md). */
|
|
121
|
+
export interface RoadmapPhaseModule {
|
|
122
|
+
name: string;
|
|
123
|
+
deliverables: string;
|
|
124
|
+
doc: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A single delivery phase parsed from implementation-roadmap.md. */
|
|
128
|
+
export interface RoadmapPhase {
|
|
129
|
+
index: number;
|
|
130
|
+
title: string;
|
|
131
|
+
goal: string;
|
|
132
|
+
days: string;
|
|
133
|
+
modules: RoadmapPhaseModule[];
|
|
134
|
+
completedModules: string[];
|
|
135
|
+
dependencies: string[];
|
|
136
|
+
migrations: string[];
|
|
137
|
+
criteria: string[];
|
|
138
|
+
status: string; // "pending" | "in_progress" | "done"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Persisted roadmap execution state (.pi/.guardian-roadmap-state.json). */
|
|
142
|
+
export interface RoadmapState {
|
|
143
|
+
phases: RoadmapPhase[];
|
|
144
|
+
currentPhaseIndex: number;
|
|
145
|
+
startedAt: string;
|
|
146
|
+
updatedAt: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
107
149
|
export type EpicState = {
|
|
108
150
|
name: string;
|
|
109
151
|
trackingIssueId: string | null;
|
|
@@ -34,10 +34,10 @@ import {
|
|
|
34
34
|
generateContractFreezeMarkdown,
|
|
35
35
|
generateIssueMarkdown,
|
|
36
36
|
generateProofingMarkdown,
|
|
37
|
+
generateRuntimeIntegrationMarkdown,
|
|
38
|
+
moduleHasRuntimeIntegration,
|
|
37
39
|
detectLanguage,
|
|
38
40
|
testBaseDirFromLang,
|
|
39
|
-
testRunnerHintFromLang,
|
|
40
|
-
implExtension,
|
|
41
41
|
} from "./architect-lib/generators";
|
|
42
42
|
import {
|
|
43
43
|
generateEpicTestFiles,
|
|
@@ -112,7 +112,7 @@ function formatEpicStatus(state: EpicState | null): string {
|
|
|
112
112
|
class EpicManager {
|
|
113
113
|
private state: EpicState | null;
|
|
114
114
|
|
|
115
|
-
constructor(private cwd: string) {
|
|
115
|
+
constructor(private readonly cwd: string) {
|
|
116
116
|
this.state = loadEpicState(cwd);
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -120,7 +120,8 @@ class EpicManager {
|
|
|
120
120
|
return this.state;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
// NOSONAR: architect orchestrator (deliberate multi-branch dispatcher)
|
|
124
|
+
async startEpic( // NOSONAR: architect orchestrator — deliberate multi-branch dispatcher
|
|
124
125
|
ctx: ExtensionContext,
|
|
125
126
|
name: string,
|
|
126
127
|
trackingIssueId?: string,
|
|
@@ -141,9 +142,33 @@ class EpicManager {
|
|
|
141
142
|
slice = { module: matchedModule.replace(".md", ""), components, nextLogicalSlice: planned };
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
|
-
// Fallback:
|
|
145
|
+
// Fallback: auto-select ONLY when unambiguous. If the epic name did not
|
|
146
|
+
// match a module doc and several modules still have planned components,
|
|
147
|
+
// error with candidates instead of silently planning the wrong module
|
|
148
|
+
// (previously this silently picked the first match — e.g. stale docs).
|
|
145
149
|
if (!slice) {
|
|
146
|
-
|
|
150
|
+
const candidates: { file: string; planned: number }[] = [];
|
|
151
|
+
for (const f of moduleFiles) {
|
|
152
|
+
const comps = parseModuleFile(join(this.cwd, ARCH_MODULES_DIR, f));
|
|
153
|
+
const planned = comps.filter((c: ModuleComponent) => c.status === "planned");
|
|
154
|
+
if (planned.length > 0) candidates.push({ file: f, planned: planned.length });
|
|
155
|
+
}
|
|
156
|
+
if (candidates.length === 1) {
|
|
157
|
+
const c = candidates[0];
|
|
158
|
+
const comps = parseModuleFile(join(this.cwd, ARCH_MODULES_DIR, c.file));
|
|
159
|
+
slice = {
|
|
160
|
+
module: c.file.replace(".md", ""),
|
|
161
|
+
components: comps,
|
|
162
|
+
nextLogicalSlice: comps.filter((x: ModuleComponent) => x.status === "planned"),
|
|
163
|
+
};
|
|
164
|
+
} else if (candidates.length > 1) {
|
|
165
|
+
const list = candidates
|
|
166
|
+
.map((c) => ` - ${c.file.replace(".md", "")} (${c.planned} planned)`)
|
|
167
|
+
.join("\n");
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Epic name "${name}" did not match any architecture module, and ${candidates.length} modules have planned components:\n${list}\nRename the epic to match a module (e.g. /architect --epic "Module Name") or mark stale module components as implemented.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
147
172
|
}
|
|
148
173
|
if (!slice) {
|
|
149
174
|
throw new Error("All architecture components are implemented. No next slice found.");
|
|
@@ -283,6 +308,30 @@ class EpicManager {
|
|
|
283
308
|
issues.push(entry);
|
|
284
309
|
}
|
|
285
310
|
|
|
311
|
+
// 2.5 Runtime Integration & Legacy Migration (ADR-009) — mandatory when
|
|
312
|
+
// the module doc documents runtime integration. Placed after the
|
|
313
|
+
// implementation issues and before proofing so "implemented but never
|
|
314
|
+
// wired" cannot be deferred by a scope note.
|
|
315
|
+
if (moduleHasRuntimeIntegration(this.cwd, slice.module)) {
|
|
316
|
+
const integrationId = "issue-runtime-integration";
|
|
317
|
+
const integrationEntry = {
|
|
318
|
+
id: integrationId,
|
|
319
|
+
title: "Runtime Integration & Legacy Migration",
|
|
320
|
+
status: "planned" as string,
|
|
321
|
+
remoteIssueId: null as string | null,
|
|
322
|
+
};
|
|
323
|
+
const integrationMd = generateRuntimeIntegrationMarkdown(slice, name, this.cwd);
|
|
324
|
+
writeFileSync(join(issuesDir, `${integrationId}.md`), integrationMd);
|
|
325
|
+
if (hasRemote && remoteRepo) {
|
|
326
|
+
const result = createRemoteIssue(this.cwd, integrationEntry.title, join(issuesDir, `${integrationId}.md`), "epic,runtime-integration", remoteRepo);
|
|
327
|
+
if (result.success && result.issueNumber) {
|
|
328
|
+
integrationEntry.remoteIssueId = result.issueNumber;
|
|
329
|
+
if (effectiveTrackingId) linkRemoteIssue(this.cwd, result.issueNumber, effectiveTrackingId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
issues.push(integrationEntry);
|
|
333
|
+
}
|
|
334
|
+
|
|
286
335
|
// 3. Proofing
|
|
287
336
|
const proofingId = "issue-proofing";
|
|
288
337
|
const proofingEntry = {
|
|
@@ -346,7 +395,10 @@ class EpicManager {
|
|
|
346
395
|
`**Components:** ${slice.nextLogicalSlice.length}`,
|
|
347
396
|
"",
|
|
348
397
|
"## Issues",
|
|
349
|
-
...issues.map((i) =>
|
|
398
|
+
...issues.map((i) => {
|
|
399
|
+
const ref = i.remoteIssueId ? ` (#${i.remoteIssueId})` : "";
|
|
400
|
+
return `- ${i.title}${ref}`;
|
|
401
|
+
}),
|
|
350
402
|
"",
|
|
351
403
|
"---",
|
|
352
404
|
"Auto-generated by Guardian Architect",
|
|
@@ -386,21 +438,31 @@ class EpicManager {
|
|
|
386
438
|
|
|
387
439
|
// ── Extension ──
|
|
388
440
|
|
|
389
|
-
export default function (pi: ExtensionAPI) {
|
|
390
|
-
let manager: EpicManager | null = null;
|
|
391
441
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
442
|
+
function randomPL(): string {
|
|
443
|
+
const buf = new Uint8Array(2);
|
|
444
|
+
(globalThis as { crypto?: { getRandomValues?: (a: Uint8Array) => void } }).crypto?.getRandomValues?.(buf);
|
|
445
|
+
const n = ((buf[0] << 8) | buf[1]) % 10000;
|
|
446
|
+
return `PL-${String(n).padStart(4, "0")}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function findFlag(tokens: string[], prefix: string): string | undefined {
|
|
450
|
+
const eqMatch = tokens.find((a) => a.startsWith(`${prefix}=`));
|
|
451
|
+
if (eqMatch) return eqMatch.split("=").slice(1).join("=");
|
|
452
|
+
const idx = tokens.indexOf(prefix);
|
|
453
|
+
if (idx >= 0 && idx + 1 < tokens.length && !tokens[idx + 1].startsWith("--")) return tokens[idx + 1];
|
|
454
|
+
return undefined;
|
|
398
455
|
}
|
|
399
456
|
|
|
457
|
+
export default function architectExtension(pi: ExtensionAPI) {
|
|
458
|
+
let manager: EpicManager | null = null;
|
|
459
|
+
|
|
460
|
+
|
|
400
461
|
pi.registerCommand("architect", {
|
|
401
462
|
description: "Orchestrate the full architecture-to-implementation process",
|
|
402
|
-
|
|
403
|
-
|
|
463
|
+
// NOSONAR: architect command dispatcher (deliberate multi-branch router)
|
|
464
|
+
handler: async function architectHandler(args: unknown, ctx: ExtensionContext) { // NOSONAR: architect command router — deliberate multi-branch dispatcher
|
|
465
|
+
manager ??= new EpicManager(ctx.cwd);
|
|
404
466
|
const raw = typeof args === "string" ? args : "";
|
|
405
467
|
const tokens = raw ? parseArgs(raw) : [];
|
|
406
468
|
if (tokens.length === 0) {
|
|
@@ -455,7 +517,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
455
517
|
}
|
|
456
518
|
|
|
457
519
|
if (tokens[0] === "--phase" || action === "phase") {
|
|
458
|
-
const phaseName = (tokens.slice(1).join(" ") || findFlag(tokens, "--phase")).replace(/["']/g, "").trim();
|
|
520
|
+
const phaseName = (tokens.slice(1).join(" ") || findFlag(tokens, "--phase") || "").replace(/["']/g, "").trim();
|
|
459
521
|
if (!phaseName) {
|
|
460
522
|
ctx.ui.notify('Usage: /architect --phase "Phase 1"', "error");
|
|
461
523
|
return;
|
|
@@ -469,9 +531,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
469
531
|
|
|
470
532
|
// Find phase by name or index
|
|
471
533
|
let targetPhase: RoadmapPhase | undefined;
|
|
472
|
-
const idxMatch =
|
|
534
|
+
const idxMatch = /Phase\s+(\d+)/i.exec(phaseName);
|
|
473
535
|
if (idxMatch) {
|
|
474
|
-
targetPhase = phases.find((p) => p.index === parseInt(idxMatch[1], 10));
|
|
536
|
+
targetPhase = phases.find((p) => p.index === Number.parseInt(idxMatch[1], 10));
|
|
475
537
|
} else {
|
|
476
538
|
targetPhase = phases.find((p) => p.title.toLowerCase().includes(phaseName.toLowerCase()));
|
|
477
539
|
}
|
|
@@ -486,7 +548,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
486
548
|
if (dep.toLowerCase() === "none") return false;
|
|
487
549
|
const depMatch = dep.match(/Phase\s+(\d+)/i);
|
|
488
550
|
if (!depMatch) return false;
|
|
489
|
-
const depPhase = phases.find((p) => p.index === parseInt(depMatch[1], 10));
|
|
551
|
+
const depPhase = phases.find((p) => p.index === Number.parseInt(depMatch[1], 10));
|
|
490
552
|
return depPhase && depPhase.status !== "done";
|
|
491
553
|
});
|
|
492
554
|
if (unmetDeps.length > 0) {
|
|
@@ -541,7 +603,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
541
603
|
}
|
|
542
604
|
const pipelineFile = join(ctx.cwd, ".pi/.guardian-pipeline-state.json");
|
|
543
605
|
if (!existsSync(pipelineFile) && existingIssues.length > 0) {
|
|
544
|
-
const pipelineId =
|
|
606
|
+
const pipelineId = randomPL();
|
|
545
607
|
const pipelineState = {
|
|
546
608
|
id: pipelineId,
|
|
547
609
|
name: mod.name,
|
|
@@ -583,10 +645,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
583
645
|
const rs = loadRoadmapState(ctx.cwd) || {
|
|
584
646
|
phases: [], currentPhaseIndex: 0, startedAt: "", updatedAt: "",
|
|
585
647
|
};
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
648
|
+
const roadmapPhase = rs.phases.find((p) => p.index === targetPhase.index);
|
|
649
|
+
if (roadmapPhase) {
|
|
650
|
+
const comp = new Set(roadmapPhase.completedModules || []);
|
|
651
|
+
comp.add(mod.name);
|
|
652
|
+
roadmapPhase.completedModules = Array.from(comp);
|
|
590
653
|
}
|
|
591
654
|
rs.updatedAt = new Date().toISOString();
|
|
592
655
|
saveRoadmapState(ctx.cwd, rs);
|
|
@@ -594,13 +657,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
594
657
|
}
|
|
595
658
|
try {
|
|
596
659
|
const state = await manager.startEpic(ctx, mod.name);
|
|
597
|
-
if (!state
|
|
660
|
+
if (!state?.slices?.length) {
|
|
598
661
|
results.push(`⚠️ ${mod.name} — no architecture components found`);
|
|
599
662
|
continue;
|
|
600
663
|
}
|
|
601
664
|
const items = (state.issues || []).map((i: { id: string }) => i.id);
|
|
602
665
|
// Create pipeline state for this epic
|
|
603
|
-
const pipelineId =
|
|
666
|
+
const pipelineId = randomPL();
|
|
604
667
|
const pipelineState = {
|
|
605
668
|
id: pipelineId,
|
|
606
669
|
name: mod.name,
|
|
@@ -674,8 +737,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
674
737
|
}
|
|
675
738
|
|
|
676
739
|
if (tokens[0] === "--phase-module-done" || action === "phase-module-done") {
|
|
677
|
-
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
678
|
-
if (isNaN(phaseIdx) || !tokens[2]) {
|
|
740
|
+
const phaseIdx = Number.parseInt(tokens[1] || "", 10);
|
|
741
|
+
if (Number.isNaN(phaseIdx) || !tokens[2]) {
|
|
679
742
|
ctx.ui.notify('Usage: /architect --phase-module-done <phase-number> "<module-name>"', "error");
|
|
680
743
|
return;
|
|
681
744
|
}
|
|
@@ -717,8 +780,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
717
780
|
}
|
|
718
781
|
|
|
719
782
|
if (tokens[0] === "--phase-done" || action === "phase-done") {
|
|
720
|
-
const phaseIdx = parseInt(tokens[1] || "", 10);
|
|
721
|
-
if (isNaN(phaseIdx)) {
|
|
783
|
+
const phaseIdx = Number.parseInt(tokens[1] || "", 10);
|
|
784
|
+
if (Number.isNaN(phaseIdx)) {
|
|
722
785
|
ctx.ui.notify('Usage: /architect --phase-done <phase-number>', "error");
|
|
723
786
|
return;
|
|
724
787
|
}
|
|
@@ -782,7 +845,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
782
845
|
|
|
783
846
|
const state = await manager.startEpic(ctx, epicName, trackingIssueId, tddEnabled);
|
|
784
847
|
|
|
785
|
-
if (!state
|
|
848
|
+
if (!state?.slices?.length) {
|
|
786
849
|
ctx.ui.notify("Failed to discover architecture components. Check .pi/architecture/modules/.", "error");
|
|
787
850
|
return;
|
|
788
851
|
}
|
|
@@ -810,7 +873,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
810
873
|
} catch { /* ignore */ }
|
|
811
874
|
|
|
812
875
|
// Write pipeline state directly (ctx.tools not available in command handlers)
|
|
813
|
-
const pipelineId =
|
|
876
|
+
const pipelineId = randomPL();
|
|
814
877
|
const pipelineState = {
|
|
815
878
|
id: pipelineId,
|
|
816
879
|
name: epicName,
|
|
@@ -841,7 +904,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
841
904
|
: "";
|
|
842
905
|
|
|
843
906
|
const firstItem = items[0];
|
|
844
|
-
const issueFilename = `${firstItem}.md`.
|
|
907
|
+
const issueFilename = `${firstItem}.md`.replaceAll("/", "-");
|
|
845
908
|
const issuePath = join(ctx.cwd, ".pi/issues", issueFilename);
|
|
846
909
|
|
|
847
910
|
let issueContent = "";
|
|
@@ -904,7 +967,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
904
967
|
description: "Show the current epic status and progress.",
|
|
905
968
|
parameters: { type: "object", properties: {} },
|
|
906
969
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
907
|
-
|
|
970
|
+
manager ??= new EpicManager(ctx.cwd);
|
|
908
971
|
const state = manager.getState();
|
|
909
972
|
return { content: [{ type: "text", text: formatEpicStatus(state) }] };
|
|
910
973
|
},
|
|
@@ -924,18 +987,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
924
987
|
for (const file of moduleFiles) {
|
|
925
988
|
const components = parseModuleFile(join(ctx.cwd, ".pi/architecture/modules", file));
|
|
926
989
|
const planned = components.filter((c) => c.status === "planned");
|
|
927
|
-
lines.push(`### ${file.replace(".md", "")}`);
|
|
928
|
-
lines.push(` Components: ${components.length} (${planned.length} planned)`);
|
|
990
|
+
lines.push(`### ${file.replace(".md", "")}`, ` Components: ${components.length} (${planned.length} planned)`);
|
|
929
991
|
if (planned.length > 0) {
|
|
930
|
-
lines.push(" Next slice:");
|
|
931
|
-
for (const c of planned) lines.push(` - ${c.name}`);
|
|
992
|
+
lines.push(" Next slice:", ...planned.map((c: ModuleComponent) => ` - ${c.name}`));
|
|
932
993
|
}
|
|
933
994
|
lines.push("");
|
|
934
995
|
}
|
|
935
996
|
const slice = findNextLogicalSlice(ctx.cwd, moduleFiles);
|
|
936
997
|
if (slice) {
|
|
937
|
-
lines.push(`**Recommended next epic:** ${slice.module}`);
|
|
938
|
-
lines.push(`Components: ${slice.nextLogicalSlice.map((c: ModuleComponent) => c.name).join(", ")}`);
|
|
998
|
+
lines.push(`**Recommended next epic:** ${slice.module}`, `Components: ${slice.nextLogicalSlice.map((c: ModuleComponent) => c.name).join(", ")}`);
|
|
939
999
|
}
|
|
940
1000
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
941
1001
|
},
|