guardian-framework 0.1.146 → 0.1.148
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/dist/cli.js +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/architecture/wiring-claims.tsv +13 -0
- package/templates/pi/coverage-floors.tsv +12 -0
- package/templates/pi/extensions/architect.ts +44 -4
- package/templates/pi/extensions/epic.ts +113 -0
- package/templates/pi/extensions/pipeline.ts +7 -1
- package/templates/pi/guards.tsv +33 -0
- package/templates/pi/mirrors.tsv +9 -0
- package/templates/pi/preflight_report.json +2 -2
- package/templates/pi/prompts/epic-parallel.md +54 -0
- package/templates/pi/script-drift-allow.tsv +13 -0
- package/templates/pi/scripts/ci/check_architecture_conformance.sh +6 -6
- package/templates/pi/scripts/ci/run_hardening_stages.sh +7 -7
- package/templates/pi/scripts/ci/run_preflight.sh +6 -6
- package/templates/pi/scripts/ci/stage_docs_policy.sh +3 -3
- package/templates/pi/scripts/ci/stage_lint.sh +2 -2
- package/templates/pi/scripts/ci/stage_remaining.sh +3 -3
- package/templates/pi/scripts/ci/stage_static_analysis.sh +4 -4
- package/templates/pi/scripts/ci/stage_unit.sh +2 -2
- package/templates/pi/scripts/ci/validate_agent_output.sh +11 -11
- package/templates/pi/scripts/epic-parallel.sh +95 -0
- package/templates/pi/scripts/epic-registry.sh +138 -0
- package/templates/pi/scripts/languages/python/validate-canonical.sh +3 -3
- package/templates/pi/scripts/validate-coverage-floors.sh +174 -0
- package/templates/pi/scripts/validate-guards.sh +171 -0
- package/templates/pi/scripts/validate-script-drift.sh +213 -0
- package/templates/pi/scripts/validate-shell-exit-status.sh +120 -0
- package/templates/pi/scripts/validate-wiring-claims.sh +228 -0
package/dist/cli.js
CHANGED
|
@@ -3206,7 +3206,7 @@ __export(exports_package, {
|
|
|
3206
3206
|
bin: () => bin,
|
|
3207
3207
|
author: () => author
|
|
3208
3208
|
});
|
|
3209
|
-
var name = "guardian-framework", version = "0.1.
|
|
3209
|
+
var name = "guardian-framework", version = "0.1.148", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
3210
3210
|
var init_package = __esm(() => {
|
|
3211
3211
|
exports = {
|
|
3212
3212
|
".": {
|
package/dist/exports.js
CHANGED
|
@@ -1991,7 +1991,7 @@ __export(exports_package, {
|
|
|
1991
1991
|
bin: () => bin,
|
|
1992
1992
|
author: () => author
|
|
1993
1993
|
});
|
|
1994
|
-
var name = "guardian-framework", version = "0.1.
|
|
1994
|
+
var name = "guardian-framework", version = "0.1.148", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
1995
1995
|
var init_package = __esm(() => {
|
|
1996
1996
|
exports = {
|
|
1997
1997
|
".": {
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Wiring claims (ADR-009): make "it is wired" claims checkable.
|
|
2
|
+
#
|
|
3
|
+
# validate-wiring-claims.sh fails both ways:
|
|
4
|
+
# * a CONFIRMED row whose evidence disappears — the wiring was removed and
|
|
5
|
+
# the doc still claims it;
|
|
6
|
+
# * a GAP row whose evidence appears (promote it) or whose module doc stops
|
|
7
|
+
# declaring the gap.
|
|
8
|
+
#
|
|
9
|
+
# Columns (tab-separated): claim module status evidence note
|
|
10
|
+
# status CONFIRMED | GAP
|
|
11
|
+
# evidence path or path:symbol that proves (or disproves) the wiring
|
|
12
|
+
#
|
|
13
|
+
# claim module status evidence note
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Coverage floors (ADR-010) — a floor is a RATCHET.
|
|
2
|
+
#
|
|
3
|
+
# Raise a floor when a layer improves; never lower one to make a build pass.
|
|
4
|
+
# validate-coverage-floors.sh fails when a floor decreases or a layer
|
|
5
|
+
# disappears, compared against the merge base.
|
|
6
|
+
#
|
|
7
|
+
# Columns (tab-separated): layer floor measured target note
|
|
8
|
+
# floor the value that gates (ratchet; never decreases)
|
|
9
|
+
# measured last measured coverage (informational)
|
|
10
|
+
# target the roadmap criterion (gap printed when unmet)
|
|
11
|
+
#
|
|
12
|
+
# layer floor measured target note
|
|
@@ -492,7 +492,7 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
492
492
|
const tokens = raw ? parseArgs(raw) : [];
|
|
493
493
|
if (tokens.length === 0) {
|
|
494
494
|
ctx.ui.notify(
|
|
495
|
-
"Usage: /architect [--epic Name] [--tracking-issue N] [--tdd] [--pr-per epic|issue] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | --phase-module-done <N> \"Module\" | status | next-epic | abort",
|
|
495
|
+
"Usage: /architect [--epic Name] [--tracking-issue N] [--tdd] [--pr-per epic|issue] [--parallel|--worktree] | --roadmap | --phase \"Phase 1\" | --phase-status | --phase-done <N> | --phase-module-done <N> \"Module\" | status | next-epic | abort",
|
|
496
496
|
"info",
|
|
497
497
|
);
|
|
498
498
|
return;
|
|
@@ -635,7 +635,7 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
635
635
|
items: existingIssues,
|
|
636
636
|
steps: [
|
|
637
637
|
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
638
|
-
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
|
|
638
|
+
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring", "guards", "drift", "shell", "claims", "floors"] } },
|
|
639
639
|
{ name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
|
|
640
640
|
{ name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
641
641
|
],
|
|
@@ -697,7 +697,7 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
697
697
|
items,
|
|
698
698
|
steps: [
|
|
699
699
|
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
700
|
-
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
|
|
700
|
+
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring", "guards", "drift", "shell", "claims", "floors"] } },
|
|
701
701
|
{ name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
|
|
702
702
|
{ name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
703
703
|
],
|
|
@@ -863,6 +863,8 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
863
863
|
// legacy per-issue PR flow.
|
|
864
864
|
const prPerFlag = (findFlag(tokens, "--pr-per") ?? "epic").toLowerCase();
|
|
865
865
|
const epicPrStrategy: "issue" | "epic" = prPerFlag === "issue" ? "issue" : "epic";
|
|
866
|
+
const useWorktree = tokens.includes("--worktree");
|
|
867
|
+
const parallelFlag = tokens.includes("--parallel");
|
|
866
868
|
try {
|
|
867
869
|
const epicStatePath = join(ctx.cwd, ".pi", ".guardian-epic-state.json");
|
|
868
870
|
const st = existsSync(epicStatePath)
|
|
@@ -887,6 +889,44 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
887
889
|
return;
|
|
888
890
|
}
|
|
889
891
|
|
|
892
|
+
// ── Parallel epics (--parallel / --worktree) ───────────────
|
|
893
|
+
// One module per worktree keeps parallel epics isolated; the
|
|
894
|
+
// contract-freeze gate refuses to start before shared contracts
|
|
895
|
+
// are frozen (epic-parallel.sh exits 2 when blocked).
|
|
896
|
+
if (parallelFlag || useWorktree) {
|
|
897
|
+
const reg = join(ctx.cwd, ".pi", "scripts", "epic-registry.sh");
|
|
898
|
+
if (!existsSync(reg)) {
|
|
899
|
+
ctx.ui.notify("epic-registry.sh not found — run `guardian update` to install parallel-epic scripts.", "error");
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const prep = runScript(ctx.cwd, `bash .pi/scripts/epic-parallel.sh "${epicName.replace(/"/g, '\\"')}"`);
|
|
903
|
+
if (prep.exitCode === 2) {
|
|
904
|
+
ctx.ui.notify(
|
|
905
|
+
`Parallel epic blocked: contracts for "${epicName}" are not frozen. Merge the Contract Freeze issue, then: bash .pi/scripts/epic-registry.sh freeze "${epicName}" --frozen`,
|
|
906
|
+
"error",
|
|
907
|
+
);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (prep.exitCode !== 0) {
|
|
911
|
+
ctx.ui.notify(`Worktree preparation failed: ${String(prep.stdout).slice(0, 300)}`, "error");
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
// Record on the epic state so later steps stay consistent.
|
|
915
|
+
try {
|
|
916
|
+
const p = join(ctx.cwd, ".pi/.guardian-epic-state.json");
|
|
917
|
+
const st = existsSync(p) ? (JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>) : {};
|
|
918
|
+
st.parallel = true;
|
|
919
|
+
st.worktree = String(prep.stdout).match(/worktree:\s*(\S+)/)?.[1] ?? "";
|
|
920
|
+
writeFileSync(p, JSON.stringify(st, null, 2), "utf-8");
|
|
921
|
+
} catch {
|
|
922
|
+
// ignore
|
|
923
|
+
}
|
|
924
|
+
ctx.ui.notify(
|
|
925
|
+
"Parallel epic prepared — worktree + epic branch created (contracts frozen).",
|
|
926
|
+
"success",
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
890
930
|
const state = await manager.startEpic(ctx, epicName, trackingIssueId, tddEnabled);
|
|
891
931
|
|
|
892
932
|
if (!state?.slices?.length) {
|
|
@@ -924,7 +964,7 @@ export default function architectExtension(pi: ExtensionAPI) {
|
|
|
924
964
|
items,
|
|
925
965
|
steps: [
|
|
926
966
|
{ name: "implement", prompt: ".pi/prompts/issue-implementation-series.md", acceptance: { type: "validator", validators: ["ci"] } },
|
|
927
|
-
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] } },
|
|
967
|
+
{ name: "validate", acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring", "guards", "drift", "shell", "claims", "floors"] } },
|
|
928
968
|
{ name: "create-mr", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "none" } },
|
|
929
969
|
{ name: "merge", prompt: ".pi/prompts/epic-closeout.md", acceptance: { type: "validator", validators: ["ci", "canonical"] } },
|
|
930
970
|
],
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Canonical Reference: .pi/architecture/modules/epic-parallel.md#epic-command
|
|
2
|
+
/**
|
|
3
|
+
* /epic — parallel epic scale-out (companion to /architect --epic).
|
|
4
|
+
*
|
|
5
|
+
* One module -> one epic branch -> one worktree -> one PR. The command shares
|
|
6
|
+
* the same registry + contract-freeze gate as /architect --parallel so both
|
|
7
|
+
* entry points behave identically.
|
|
8
|
+
*
|
|
9
|
+
* /epic parallel "<Module>" create worktree + branch (gated)
|
|
10
|
+
* /epic freeze "<Module>" record that contracts are frozen
|
|
11
|
+
* /epic status list registered epics (all worktrees)
|
|
12
|
+
* /epic prompt show the scale-out procedure
|
|
13
|
+
*/
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
type ExtensionContext = { cwd: string; ui: { notify(message: string, level?: string): void } };
|
|
19
|
+
type ExtensionAPI = {
|
|
20
|
+
registerCommand(
|
|
21
|
+
name: string,
|
|
22
|
+
options: { description: string; handler(args: unknown, ctx: ExtensionContext): unknown },
|
|
23
|
+
): void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function unquote(s: string): string {
|
|
27
|
+
return s.trim().replace(/^["']|["']$/g, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function run(cwd: string, cmd: string): { code: number; out: string } {
|
|
31
|
+
try {
|
|
32
|
+
const out = execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
33
|
+
return { code: 0, out };
|
|
34
|
+
} catch (e) {
|
|
35
|
+
const err = e as { status?: number; stdout?: string; stderr?: string };
|
|
36
|
+
return { code: err.status ?? 1, out: `${err.stdout ?? ""}${err.stderr ?? ""}` };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function epicExtension(pi: ExtensionAPI) {
|
|
41
|
+
pi.registerCommand("epic", {
|
|
42
|
+
description: "Parallel epics: one worktree per module, gated on frozen contracts",
|
|
43
|
+
handler: async (args: unknown, ctx: ExtensionContext) => {
|
|
44
|
+
const raw = String(args ?? "").trim();
|
|
45
|
+
const [action, ...rest] = raw.split(/\s+/);
|
|
46
|
+
const moduleName = unquote(rest.join(" "));
|
|
47
|
+
|
|
48
|
+
if (!action) {
|
|
49
|
+
ctx.ui.notify(
|
|
50
|
+
[
|
|
51
|
+
"Usage:",
|
|
52
|
+
' /epic parallel "<Module>" create worktree + epic branch (requires frozen contracts)',
|
|
53
|
+
' /epic freeze "<Module>" record that shared contracts are frozen',
|
|
54
|
+
" /epic status list registered epics across worktrees",
|
|
55
|
+
" /epic prompt show the scale-out procedure",
|
|
56
|
+
].join("\n"),
|
|
57
|
+
"info",
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (action === "freeze") {
|
|
63
|
+
if (!moduleName) {
|
|
64
|
+
ctx.ui.notify('Usage: /epic freeze "<Module>"', "error");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const r = run(ctx.cwd, `bash .pi/scripts/epic-registry.sh freeze "${moduleName}" --frozen`);
|
|
68
|
+
ctx.ui.notify(r.code === 0 ? `Contracts frozen for "${moduleName}"` : r.out.slice(0, 300), r.code === 0 ? "success" : "error");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (action === "parallel") {
|
|
73
|
+
if (!moduleName) {
|
|
74
|
+
ctx.ui.notify('Usage: /epic parallel "<Module>"', "error");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const r = run(ctx.cwd, `bash .pi/scripts/epic-parallel.sh "${moduleName}"`);
|
|
78
|
+
if (r.code === 2) {
|
|
79
|
+
ctx.ui.notify(
|
|
80
|
+
`Blocked: contracts for "${moduleName}" are not frozen. Merge the Contract Freeze issue, then /epic freeze "${moduleName}".`,
|
|
81
|
+
"error",
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
ctx.ui.notify(
|
|
86
|
+
r.code === 0
|
|
87
|
+
? `Worktree ready for "${moduleName}". Next: cd into it and run /architect --epic "${moduleName}"`
|
|
88
|
+
: r.out.slice(0, 400),
|
|
89
|
+
r.code === 0 ? "success" : "error",
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (action === "status") {
|
|
95
|
+
const r = run(ctx.cwd, "bash .pi/scripts/epic-registry.sh list");
|
|
96
|
+
ctx.ui.notify(r.out || "(no epics registered)", "info");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (action === "prompt") {
|
|
101
|
+
const p = join(ctx.cwd, ".pi", "prompts", "epic-parallel.md");
|
|
102
|
+
if (existsSync(p)) {
|
|
103
|
+
ctx.ui.notify(`Procedure: ${p}`, "info");
|
|
104
|
+
return readFileSync(p, "utf-8");
|
|
105
|
+
}
|
|
106
|
+
ctx.ui.notify("Run `bash .pi/scripts/epic-parallel.sh <Module>` after freezing contracts.", "info");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ctx.ui.notify(`Unknown /epic action "${action}". Try /epic for usage.`, "error");
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -45,6 +45,12 @@ const VALIDATOR_SCRIPTS: Record<string, string> = {
|
|
|
45
45
|
integration: ".pi/scripts/validate-integration.sh",
|
|
46
46
|
// ADR-009: implemented modules must be INSTALLED in the composition root
|
|
47
47
|
wiring: ".pi/scripts/check_composition_root.sh",
|
|
48
|
+
// ADR-010: controls that make the other controls enforceable
|
|
49
|
+
guards: ".pi/scripts/validate-guards.sh",
|
|
50
|
+
drift: ".pi/scripts/validate-script-drift.sh",
|
|
51
|
+
claims: ".pi/scripts/validate-wiring-claims.sh",
|
|
52
|
+
floors: ".pi/scripts/validate-coverage-floors.sh",
|
|
53
|
+
shell: ".pi/scripts/validate-shell-exit-status.sh",
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
// ── Helpers ──
|
|
@@ -1445,7 +1451,7 @@ function buildSteps(stepNames: string[]): StepConfig[] {
|
|
|
1445
1451
|
},
|
|
1446
1452
|
validate: {
|
|
1447
1453
|
name: "validate",
|
|
1448
|
-
acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring"] },
|
|
1454
|
+
acceptance: { type: "validator", validators: ["ci", "tests", "security", "wiring", "guards", "drift", "shell", "claims", "floors"] },
|
|
1449
1455
|
},
|
|
1450
1456
|
"create-mr": {
|
|
1451
1457
|
name: "create-mr",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Self-owned guard registry (ADR-010).
|
|
2
|
+
#
|
|
3
|
+
# Every root .pi/scripts/validate-*.sh MUST appear here. `validate-guards.sh`
|
|
4
|
+
# fails when a guard exists but is unclassified, so a new guard cannot be
|
|
5
|
+
# silently left un-gated.
|
|
6
|
+
#
|
|
7
|
+
# Columns (tab-separated):
|
|
8
|
+
# guard path to the guard script, relative to the repo root
|
|
9
|
+
# class the pattern class it owns
|
|
10
|
+
# tier gating | advisory
|
|
11
|
+
# gating = a non-zero exit fails a mandatory path
|
|
12
|
+
# advisory = informational; it does not stop a merge
|
|
13
|
+
# run_here yes | no (yes = `validate-guards.sh --run` executes it)
|
|
14
|
+
# owner who is accountable for the class
|
|
15
|
+
# notes how it gates / what it does
|
|
16
|
+
#
|
|
17
|
+
# guard class tier run_here owner notes
|
|
18
|
+
.pi/scripts/validate-guards.sh guards advisory no architecture meta: registry integrity; --run executes the gating set
|
|
19
|
+
.pi/scripts/validate-script-drift.sh script-drift gating yes architecture mirrored .pi/scripts differ and are unrecorded
|
|
20
|
+
.pi/scripts/validate-wiring-claims.sh wiring-claims gating yes architecture wiring claims need live evidence; gaps must stay documented
|
|
21
|
+
.pi/scripts/validate-coverage-floors.sh coverage-floors gating yes quality a coverage floor may not be lowered or removed
|
|
22
|
+
.pi/scripts/validate-shell-exit-status.sh shell-exit gating yes quality arithmetic statement returns 1 on a passing path
|
|
23
|
+
.pi/scripts/validate-architecture.sh architecture gating yes architecture dispatches the per-language architecture checks
|
|
24
|
+
.pi/scripts/validate-canonical.sh canonical gating yes architecture canonical headers and references resolve
|
|
25
|
+
.pi/scripts/validate-tests.sh tests gating yes quality tests exist, run and cover the contract
|
|
26
|
+
.pi/scripts/validate-security.sh security gating yes security secret scan and unsafe-pattern gates
|
|
27
|
+
.pi/scripts/validate-ci.sh ci gating yes platform CI pipeline matches the blueprint
|
|
28
|
+
.pi/scripts/validate-integration.sh integration gating yes architecture components integrate through their declared seams
|
|
29
|
+
.pi/scripts/validate-operations.sh operations gating yes platform readiness: observability, error handling, cancellation
|
|
30
|
+
.pi/scripts/validate-runtime-wiring.sh runtime-wiring gating yes architecture implemented modules are reachable at runtime
|
|
31
|
+
.pi/scripts/validate-architecture-readiness.sh readiness advisory no architecture architecture docs are complete enough to implement
|
|
32
|
+
.pi/scripts/validate-exploration-depth.sh exploration-depth advisory no architecture domain exploration depth is sufficient
|
|
33
|
+
.pi/scripts/validate-ubiquitous-language.sh ubiquitous-language gating yes architecture code and docs share the canonical glossary
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Extra trees compared by validate-script-drift.sh (one path per line).
|
|
2
|
+
#
|
|
3
|
+
Each path is a directory containing a `scripts/` subtree whose files are
|
|
4
|
+
compared against every other tree by path. The repo root .pi/ and every
|
|
5
|
+
immediate subdirectory carrying .pi/scripts are always compared; list here
|
|
6
|
+
trees that are NOT in that shape.
|
|
7
|
+
#
|
|
8
|
+
This repo ships a canonical tree (templates/pi) and mirrors it to .pi/.
|
|
9
|
+
templates/pi
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"timestamp": "2026-11-
|
|
2
|
+
"timestamp": "2026-11-07T14:10:40Z",
|
|
3
3
|
"mode": "all",
|
|
4
4
|
"stages_run": [],
|
|
5
5
|
"summary": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"failed": 1,
|
|
9
9
|
"skipped": 12
|
|
10
10
|
},
|
|
11
|
-
"duration_seconds":
|
|
11
|
+
"duration_seconds": 0,
|
|
12
12
|
"results": [
|
|
13
13
|
{
|
|
14
14
|
"name": "check_mr_traceability.sh",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Epic Scale-out — start parallel epics (one worktree per module)
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
Canonical Reference: .pi/prompts/epic-parallel.md
|
|
5
|
+
Implements: parallel epics (/architect --parallel / --worktree and /epic)
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
**Purpose:** Run several module epics at the same time without them colliding on
|
|
9
|
+
shared contracts. One module → one epic branch → one worktree → one PR.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Preconditions
|
|
14
|
+
|
|
15
|
+
1. **Contracts frozen.** The module's shared contracts (ports, DTOs, error envelope,
|
|
16
|
+
composition-root seam) are merged to the base branch. Parallel epics must never
|
|
17
|
+
redefine shared contracts — that is what produces merge conflicts and rework.
|
|
18
|
+
2. Clean base: `git status` clean, `main` up to date.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Procedure (per module, can be repeated in parallel)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 1. Record the contract freeze (done once, after the freeze issue merges)
|
|
26
|
+
bash .pi/scripts/epic-registry.sh freeze "<Module>" --frozen
|
|
27
|
+
|
|
28
|
+
# 2. Create the worktree + epic branch (blocked with exit 2 if not frozen)
|
|
29
|
+
bash .pi/scripts/epic-parallel.sh "<Module>"
|
|
30
|
+
|
|
31
|
+
# 3. In the new worktree, start the epic (epic mode: one branch, one PR)
|
|
32
|
+
cd ../<repo>-<module>
|
|
33
|
+
/architect --epic "<Module>"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Repeat steps 2–3 for each additional module — the worktrees are isolated, so the
|
|
37
|
+
epics run concurrently. Each epic still commits only to its own `epic/<module>`
|
|
38
|
+
branch and opens exactly one PR at the end (epic mode).
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
|
|
44
|
+
- **Never** start two epics for the same module in parallel.
|
|
45
|
+
- **Never** bypass the freeze gate; if it blocks, run the Contract Freeze issue first.
|
|
46
|
+
- Register/new worktrees are visible across all worktrees via the shared registry
|
|
47
|
+
(`.git/guardian/epics.json`):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bash .pi/scripts/epic-registry.sh list
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- Merge order: freeze branches first, then epics in dependency order (see the
|
|
54
|
+
roadmap `### Dependencies`). An epic whose dependency is not merged must wait.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Recorded per-tree differences for mirrored .pi/scripts (ADR-010).
|
|
2
|
+
#
|
|
3
|
+
# validate-script-drift.sh fails when a file mirrored across trees (the repo
|
|
4
|
+
# root plus any subdirectory carrying a .pi/ directory) differs and is NOT
|
|
5
|
+
# matched by a pattern here.
|
|
6
|
+
#
|
|
7
|
+
# Columns (tab-separated): pattern trees reason
|
|
8
|
+
# pattern glob matched against the path relative to <tree>/.pi/scripts
|
|
9
|
+
# trees which trees carry the file (informational)
|
|
10
|
+
# reason why the difference is tolerated
|
|
11
|
+
#
|
|
12
|
+
# pattern trees reason
|
|
13
|
+
ci/stage_security.sh templates/pi,.pi guardian generates a project-specific security stage (secret scan + dep audit) into .pi/scripts/ci; the template carries the dispatcher stub, same as stage_build.sh and stage_test.sh
|
|
@@ -149,7 +149,7 @@ check_tenant_isolation() {
|
|
|
149
149
|
has_tenant_filter=true
|
|
150
150
|
fi
|
|
151
151
|
if [[ "$has_tenant_filter" == "false" ]]; then
|
|
152
|
-
((violations
|
|
152
|
+
violations=$((violations + 1))
|
|
153
153
|
log_fail "$check_name" "Model $(basename "$model_file") has tenant_id but no tenant-scoped queries"
|
|
154
154
|
fi
|
|
155
155
|
fi
|
|
@@ -521,7 +521,7 @@ check_runtime_baseline() {
|
|
|
521
521
|
while IFS= read -r var; do
|
|
522
522
|
var_name=$(echo "$var" | cut -d= -f1)
|
|
523
523
|
if [[ -z "${!var_name:-}" ]]; then
|
|
524
|
-
((unbound
|
|
524
|
+
unbound=$((unbound + 1))
|
|
525
525
|
fi
|
|
526
526
|
done < <(grep -v "^#" .env.example 2>/dev/null | grep "=" || true)
|
|
527
527
|
if [[ $unbound -eq 0 ]]; then
|
|
@@ -603,7 +603,7 @@ check_arch_sanity() {
|
|
|
603
603
|
local orphaned_imports=0
|
|
604
604
|
for f in $(find . -name "*.py" -o -name "*.ts" -o -name "*.rs" -o -name "*.go" 2>/dev/null | head -30); do
|
|
605
605
|
if grep -qE "^import.*UNUSED|^from.*UNUSED" "$f" 2>/dev/null; then
|
|
606
|
-
((orphaned_imports
|
|
606
|
+
orphaned_imports=$((orphaned_imports + 1))
|
|
607
607
|
fi
|
|
608
608
|
done
|
|
609
609
|
if [[ $orphaned_imports -gt 0 ]]; then
|
|
@@ -636,7 +636,7 @@ check_arch_sanity() {
|
|
|
636
636
|
example_val=$(grep "^${var_name}=" ".env.example" | cut -d= -f2-)
|
|
637
637
|
actual_val=$(grep "^${var_name}=" ".env" | cut -d= -f2-)
|
|
638
638
|
if [[ "$example_val" == "$actual_val" ]]; then
|
|
639
|
-
((collisions
|
|
639
|
+
collisions=$((collisions + 1))
|
|
640
640
|
fi
|
|
641
641
|
fi
|
|
642
642
|
done < <(grep -v "^#" .env.example 2>/dev/null | grep "=" || true)
|
|
@@ -683,13 +683,13 @@ check_import_boundaries() {
|
|
|
683
683
|
[[ -f "$file" ]] || continue
|
|
684
684
|
if [[ "$layer_dir" == *"domain" ]]; then
|
|
685
685
|
if grep -qE "from.*infrastructure|from.*api|from.*interfaces|import.*infrastructure|import.*api|import.*interfaces" "$file" 2>/dev/null; then
|
|
686
|
-
((violations
|
|
686
|
+
violations=$((violations + 1))
|
|
687
687
|
log_fail "$check_name" "Domain layer imports from outer layer in $(basename "$file")"
|
|
688
688
|
fi
|
|
689
689
|
fi
|
|
690
690
|
if [[ "$layer_dir" == *"application" ]]; then
|
|
691
691
|
if grep -qE "from.*api|from.*interfaces|import.*api|import.*interfaces" "$file" 2>/dev/null; then
|
|
692
|
-
((violations
|
|
692
|
+
violations=$((violations + 1))
|
|
693
693
|
log_fail "$check_name" "Application layer imports from api/interfaces in $(basename "$file")"
|
|
694
694
|
fi
|
|
695
695
|
fi
|
|
@@ -102,14 +102,14 @@ run_stage() {
|
|
|
102
102
|
return 0
|
|
103
103
|
fi
|
|
104
104
|
|
|
105
|
-
((TOTAL_STAGES
|
|
105
|
+
TOTAL_STAGES=$((TOTAL_STAGES + 1))
|
|
106
106
|
|
|
107
107
|
# Check conditional
|
|
108
108
|
case "$conditional" in
|
|
109
109
|
main_only)
|
|
110
110
|
if [[ "${CI_COMMIT_BRANCH:-}" != "main" && "${GITHUB_REF_NAME:-}" != "main" ]]; then
|
|
111
111
|
log_stage_skip "$stage_name" "Not on main branch"
|
|
112
|
-
((SKIPPED_STAGES
|
|
112
|
+
SKIPPED_STAGES=$((SKIPPED_STAGES + 1))
|
|
113
113
|
return 0
|
|
114
114
|
fi
|
|
115
115
|
;;
|
|
@@ -122,14 +122,14 @@ run_stage() {
|
|
|
122
122
|
fi
|
|
123
123
|
if [[ "$has_migration" == "false" ]]; then
|
|
124
124
|
log_stage_skip "$stage_name" "No migration changes detected"
|
|
125
|
-
((SKIPPED_STAGES
|
|
125
|
+
SKIPPED_STAGES=$((SKIPPED_STAGES + 1))
|
|
126
126
|
return 0
|
|
127
127
|
fi
|
|
128
128
|
;;
|
|
129
129
|
sonar_required)
|
|
130
130
|
if [[ -z "${SONAR_TOKEN:-}" || -z "${SONAR_HOST_URL:-}" ]]; then
|
|
131
131
|
log_stage_skip "$stage_name" "SONAR_TOKEN or SONAR_HOST_URL not set"
|
|
132
|
-
((SKIPPED_STAGES
|
|
132
|
+
SKIPPED_STAGES=$((SKIPPED_STAGES + 1))
|
|
133
133
|
return 0
|
|
134
134
|
fi
|
|
135
135
|
;;
|
|
@@ -139,21 +139,21 @@ run_stage() {
|
|
|
139
139
|
|
|
140
140
|
if [[ ! -f "$stage_script" ]]; then
|
|
141
141
|
log_stage_skip "$stage_name" "Script not found: $stage_script"
|
|
142
|
-
((SKIPPED_STAGES
|
|
142
|
+
SKIPPED_STAGES=$((SKIPPED_STAGES + 1))
|
|
143
143
|
return 0
|
|
144
144
|
fi
|
|
145
145
|
|
|
146
146
|
local output
|
|
147
147
|
if output=$(bash "$stage_script" 2>&1); then
|
|
148
148
|
log_stage_pass "$stage_name"
|
|
149
|
-
((PASSED_STAGES
|
|
149
|
+
PASSED_STAGES=$((PASSED_STAGES + 1))
|
|
150
150
|
STAGE_RESULTS+=("PASS:$stage_name")
|
|
151
151
|
if [[ "$VERBOSE" == "true" ]]; then
|
|
152
152
|
echo "$output"
|
|
153
153
|
fi
|
|
154
154
|
else
|
|
155
155
|
log_stage_fail "$stage_name" "$output"
|
|
156
|
-
((FAILED_STAGES
|
|
156
|
+
FAILED_STAGES=$((FAILED_STAGES + 1))
|
|
157
157
|
STAGE_RESULTS+=("FAIL:$stage_name:$(echo "$output" | head -5)")
|
|
158
158
|
fi
|
|
159
159
|
}
|
|
@@ -65,8 +65,8 @@ log_pass() {
|
|
|
65
65
|
if [[ "$JSON" == "false" ]]; then
|
|
66
66
|
echo -e " ${GREEN}[PASS]${NC} $1 (${2}s)"
|
|
67
67
|
fi
|
|
68
|
-
((PASSED
|
|
69
|
-
((TOTAL
|
|
68
|
+
PASSED=$((PASSED + 1))
|
|
69
|
+
TOTAL=$((TOTAL + 1))
|
|
70
70
|
RESULTS+=("{\"name\": \"$1\", \"status\": \"pass\", \"message\": \"\", \"duration\": $2}")
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -74,8 +74,8 @@ log_fail() {
|
|
|
74
74
|
if [[ "$JSON" == "false" ]]; then
|
|
75
75
|
echo -e " ${RED}[FAIL]${NC} $1 — $2 (${3}s)"
|
|
76
76
|
fi
|
|
77
|
-
((FAILED
|
|
78
|
-
((TOTAL
|
|
77
|
+
FAILED=$((FAILED + 1))
|
|
78
|
+
TOTAL=$((TOTAL + 1))
|
|
79
79
|
RESULTS+=("{\"name\": \"$1\", \"status\": \"fail\", \"message\": \"$2\", \"duration\": $3}")
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -83,8 +83,8 @@ log_skip() {
|
|
|
83
83
|
if [[ "$JSON" == "false" ]]; then
|
|
84
84
|
echo -e " ${YELLOW}[SKIP]${NC} $1 — $2"
|
|
85
85
|
fi
|
|
86
|
-
((SKIPPED
|
|
87
|
-
((TOTAL
|
|
86
|
+
SKIPPED=$((SKIPPED + 1))
|
|
87
|
+
TOTAL=$((TOTAL + 1))
|
|
88
88
|
RESULTS+=("{\"name\": \"$1\", \"status\": \"skip\", \"message\": \"$2\", \"duration\": 0}")
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -13,8 +13,8 @@ ARCH_DIR="${PI_DIR}/architecture"
|
|
|
13
13
|
PASS=0
|
|
14
14
|
FAIL=0
|
|
15
15
|
|
|
16
|
-
log_pass() { echo " ✓ PASS: $1"; ((PASS
|
|
17
|
-
log_fail() { echo " ✗ FAIL: $1 — $2"; ((FAIL
|
|
16
|
+
log_pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
|
|
17
|
+
log_fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
|
|
18
18
|
|
|
19
19
|
echo " Checking MR traceability..."
|
|
20
20
|
|
|
@@ -26,7 +26,7 @@ if command -v git &>/dev/null; then
|
|
|
26
26
|
files_without_ref=0
|
|
27
27
|
while IFS= read -r file; do
|
|
28
28
|
if ! grep -q "Canonical Reference\|canonical.*reference" "$file" 2>/dev/null; then
|
|
29
|
-
((files_without_ref
|
|
29
|
+
files_without_ref=$((files_without_ref + 1))
|
|
30
30
|
log_fail "MR traceability" "${file} missing canonical reference to architecture"
|
|
31
31
|
fi
|
|
32
32
|
done <<< "$changed_files"
|
|
@@ -11,8 +11,8 @@ set -euo pipefail
|
|
|
11
11
|
FAIL=0
|
|
12
12
|
PASS=0
|
|
13
13
|
|
|
14
|
-
log_pass() { echo " ✓ PASS: $1"; ((PASS
|
|
15
|
-
log_fail() { echo " ✗ FAIL: $1 — $2"; ((FAIL
|
|
14
|
+
log_pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
|
|
15
|
+
log_fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
|
|
16
16
|
|
|
17
17
|
echo " Running lint checks..."
|
|
18
18
|
|
|
@@ -13,8 +13,8 @@ PI_DIR=".pi"
|
|
|
13
13
|
FAIL=0
|
|
14
14
|
PASS=0
|
|
15
15
|
|
|
16
|
-
log_pass() { echo " ✓ PASS: $1"; ((PASS
|
|
17
|
-
log_fail() { echo " ✗ FAIL: $1 — $2"; ((FAIL
|
|
16
|
+
log_pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
|
|
17
|
+
log_fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
|
|
18
18
|
|
|
19
19
|
STAGE="${1:-all}"
|
|
20
20
|
|
|
@@ -69,7 +69,7 @@ run_stage_7_security() {
|
|
|
69
69
|
local secrets_found=0
|
|
70
70
|
for pattern in "sk-[A-Za-z0-9]{32,}" "ghp_[A-Za-z0-9]{36}" "AKIA[0-9A-Z]{16}" "BEGIN (RSA |EC )?PRIVATE KEY"; do
|
|
71
71
|
if grep -rE "$pattern" . --include="*.py" --include="*.ts" --include="*.env" 2>/dev/null | grep -v ".git" | grep -v "node_modules" | head -1 | grep -q .; then
|
|
72
|
-
((secrets_found
|
|
72
|
+
secrets_found=$((secrets_found + 1))
|
|
73
73
|
log_fail "secret scan" "Potential secret pattern detected: $pattern"
|
|
74
74
|
fi
|
|
75
75
|
done
|
|
@@ -12,8 +12,8 @@ set -euo pipefail
|
|
|
12
12
|
FAIL=0
|
|
13
13
|
PASS=0
|
|
14
14
|
|
|
15
|
-
log_pass() { echo " ✓ PASS: $1"; ((PASS
|
|
16
|
-
log_fail() { echo " ✗ FAIL: $1 — $2"; ((FAIL
|
|
15
|
+
log_pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
|
|
16
|
+
log_fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
|
|
17
17
|
|
|
18
18
|
echo " Running static analysis..."
|
|
19
19
|
|
|
@@ -61,7 +61,7 @@ else
|
|
|
61
61
|
if [[ -d "app/domain" ]]; then
|
|
62
62
|
while IFS= read -r file; do
|
|
63
63
|
if grep -qE "from.*infrastructure|from.*api|import.*infrastructure" "$file" 2>/dev/null; then
|
|
64
|
-
((violations
|
|
64
|
+
violations=$((violations + 1))
|
|
65
65
|
log_fail "import boundary" "$(basename "$file") imports from infrastructure/api"
|
|
66
66
|
fi
|
|
67
67
|
done < <(find app/domain -name "*.py" 2>/dev/null)
|
|
@@ -93,7 +93,7 @@ if [[ -f ".env.example" && -f ".env" ]]; then
|
|
|
93
93
|
example_val=$(grep "^${var_name}=" ".env.example" 2>/dev/null | cut -d= -f2-)
|
|
94
94
|
actual_val=$(grep "^${var_name}=" ".env" 2>/dev/null | cut -d= -f2-)
|
|
95
95
|
if [[ "$example_val" == "$actual_val" && -n "$example_val" ]]; then
|
|
96
|
-
((collisions
|
|
96
|
+
collisions=$((collisions + 1))
|
|
97
97
|
log_fail "env collision" "${var_name} in .env matches .env.example value"
|
|
98
98
|
fi
|
|
99
99
|
fi
|