@pmelab/gtd 24.3.0 → 25.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-plugin/plugin.json +1 -1
- package/README.md +28 -22
- package/dist/chunks/{reserved-DJwU5C4z.mjs → reserved-6pAgFGgB.mjs} +119 -135
- package/dist/chunks/{steering-D-PvRC5W.mjs → steering-DVILanVf.mjs} +16 -5
- package/dist/flows.mjs +9 -1
- package/dist/gtd.bundle.mjs +780 -813
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +666 -161
- package/dist/workflow-kit.mjs +55 -96
- package/dist/workflows/feature.mjs +171 -76
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +1 -1
- package/dist/workflows.mjs +353 -310
- package/package.json +1 -2
- package/src/cli/Program.test.ts +3 -8
- package/src/cli/Program.ts +1 -8
- package/src/edge/Edge.test.ts +5 -2
- package/src/edge/Edge.ts +3 -4
- package/src/emit/index.ts +1 -0
- package/src/flows/scripts.ts +6 -0
- package/src/platform/Git.test.ts +6 -9
- package/src/replay/Replay.ts +2 -1
- package/src/steering/FORMATS.md +1 -1
- package/src/steering/SteeringFormat.test.ts +9 -1
- package/src/steering/SteeringFormat.ts +7 -0
- package/src/steering/contract.test.ts +3 -0
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +30 -0
- package/src/steering/review.ts +8 -2
- package/src/testkit/EmittedScriptRecognizer.test.ts +29 -30
- package/src/testkit/EmittedScriptRecognizer.ts +148 -51
- package/src/testkit/EmittedScripts.test.ts +166 -0
- package/src/testkit/FlowScripts.test.ts +242 -0
- package/src/testkit/FlowScripts.ts +326 -0
- package/src/testkit/GitTiers.ts +140 -49
- package/src/testkit/InMemRepo.ts +35 -2
- package/src/testkit/Layers.ts +10 -2
- package/src/testkit/TestRepo.ts +4 -1
- package/src/testkit/index.ts +1 -1
- package/src/workflow/discovery.test.ts +147 -0
- package/src/workflow/discovery.ts +5 -0
- package/src/workflow/fingerprint.test.ts +20 -4
- package/src/workflow/fingerprint.ts +8 -9
- package/src/workflow/index.ts +1 -6
- package/src/workflow/load.test.ts +5 -19
- package/src/workflow/load.ts +94 -10
- package/src/workflow-kit/review.test.ts +84 -0
- package/src/workflow-kit/review.ts +9 -6
- package/src/workflow-kit/scenarios.test.ts +116 -0
- package/src/workflow-kit/scenarios.ts +53 -113
- package/src/workflows/feature/feature.ts +2 -5
- package/src/workflows/feature/packages.test.ts +17 -34
- package/src/workflows/feature/packages.ts +13 -52
- package/src/workflows/feature/planning.ts +1 -1
- package/src/workflows/feature/review.test.ts +233 -54
- package/src/workflows/feature/review.ts +178 -41
- package/src/workflows/feature/steps.test.ts +12 -0
- package/src/workflows/feature/steps.ts +8 -1
- package/src/workflows/index.ts +0 -2
- package/src/workflows/shared/access.test.ts +4 -2
- package/src/workflows/shared/access.ts +1 -0
- package/src/workflows/shared/health.test.ts +1 -21
- package/src/workflows/shared/health.ts +4 -39
- package/src/workflows/shared/options.test.ts +8 -0
- package/src/workflows/shared/options.ts +1 -1
- package/src/workflows/shared/steps.ts +18 -11
- package/src/workflows/shared/tail.test.ts +1 -41
- package/src/workflows/shared/tail.ts +17 -41
- package/src/workflows/shared/text.fixture.ts +1 -0
- package/src/workflows/shared/text.test.ts +129 -41
- package/src/workflows/shared/text.ts +111 -75
- package/src/workflows/shared/vars.ts +1 -1
- package/src/workflows/shared/scenarios.test.ts +0 -256
- package/src/workflows/shared/scenarios.ts +0 -35
|
@@ -1,130 +1,70 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Change } from "../flows/index.js"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const DOC_STRING = /^(\s*)("""|```)/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
|
|
13
|
-
* dropped — except inside a doc string, whose content is test input: kept
|
|
14
|
-
* whole, indented relative to its opening delimiter (as Gherkin reads it).
|
|
15
|
-
*/
|
|
16
|
-
export const scenarioText = (source: string): string => {
|
|
17
|
-
const out: string[] = []
|
|
18
|
-
let doc: { readonly indent: number; readonly fence: string } | undefined
|
|
19
|
-
for (const line of source.split("\n")) {
|
|
20
|
-
const delimiter = DOC_STRING.exec(line)
|
|
21
|
-
if (doc === undefined && delimiter !== null) {
|
|
22
|
-
doc = { indent: delimiter[1]!.length, fence: delimiter[2]! }
|
|
23
|
-
out.push(line.trim())
|
|
24
|
-
} else if (doc !== undefined && line.trim().startsWith(doc.fence)) {
|
|
25
|
-
doc = undefined
|
|
26
|
-
out.push(line.trim())
|
|
27
|
-
} else if (doc !== undefined) {
|
|
28
|
-
const lead = line.length - line.trimStart().length
|
|
29
|
-
out.push(line.slice(Math.min(lead, doc.indent)).trimEnd())
|
|
30
|
-
} else {
|
|
31
|
-
const trimmed = line.trim()
|
|
32
|
-
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return out.join("\n")
|
|
36
|
-
}
|
|
3
|
+
const DOC_STRING = /^("""|```)/
|
|
4
|
+
const THEN = /^Then\s/
|
|
5
|
+
const CONTINUATION = /^(And|But|\*)\s/
|
|
6
|
+
const HEADING = /^(Feature|Rule|Background|Scenario|Scenario Outline|Scenario Template|Example):/
|
|
37
7
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
8
|
+
export interface Assertion {
|
|
9
|
+
/** The trimmed heading line of the scenario (or background) it sits in. */
|
|
10
|
+
readonly scenario: string
|
|
11
|
+
readonly line: string
|
|
41
12
|
}
|
|
42
13
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (text !== undefined) texts[path] = text
|
|
14
|
+
/** Trimmed lines outside doc strings, whose content is test input, not steps. */
|
|
15
|
+
const stepLines = (source: string): string[] => {
|
|
16
|
+
const out: string[] = []
|
|
17
|
+
let fence: string | undefined
|
|
18
|
+
for (const trimmed of source.split("\n").map((l) => l.trim())) {
|
|
19
|
+
const opening = fence === undefined ? DOC_STRING.exec(trimmed)?.[1] : undefined
|
|
20
|
+
if (opening !== undefined) fence = opening
|
|
21
|
+
else if (fence === undefined) out.push(trimmed)
|
|
22
|
+
else if (trimmed.startsWith(fence)) fence = undefined
|
|
53
23
|
}
|
|
54
|
-
return
|
|
24
|
+
return out
|
|
55
25
|
}
|
|
56
26
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Object.entries(frozen.texts)
|
|
60
|
-
.filter(([path, text]) => textOf(path) !== text)
|
|
61
|
-
.map(([path]) => path)
|
|
62
|
-
|
|
63
|
-
// Blank lines survive only inside doc strings, where they are wording.
|
|
64
|
-
const lines = (text: string | undefined): string[] =>
|
|
65
|
-
text === undefined || text === "" ? [] : text.split("\n")
|
|
66
|
-
|
|
67
|
-
/** `table[i][j]`: the longest common subsequence of `a[i..]` and `b[j..]`. */
|
|
68
|
-
const lcsTable = (a: readonly string[], b: readonly string[]): number[][] => {
|
|
69
|
-
const table = Array.from({ length: a.length + 1 }, () => Array<number>(b.length + 1).fill(0))
|
|
70
|
-
for (let i = a.length - 1; i >= 0; i--) {
|
|
71
|
-
for (let j = b.length - 1; j >= 0; j--) {
|
|
72
|
-
table[i]![j] =
|
|
73
|
-
a[i] === b[j] ? table[i + 1]![j + 1]! + 1 : Math.max(table[i + 1]![j]!, table[i]![j + 1]!)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return table
|
|
77
|
-
}
|
|
27
|
+
const neutral = (line: string): boolean =>
|
|
28
|
+
line === "" || line.startsWith("#") || line.startsWith("|")
|
|
78
29
|
|
|
79
|
-
/**
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} else if (table[i + 1]![j]! >= table[i]![j + 1]!) out.push(`- ${a[i++]}`)
|
|
90
|
-
else out.push(`+ ${b[j++]}`)
|
|
30
|
+
/** Trimmed `Then` step lines, an `And`/`But`/`*` continuing one included; doc strings skipped. */
|
|
31
|
+
export const assertionLines = (source: string): readonly Assertion[] => {
|
|
32
|
+
const out: Assertion[] = []
|
|
33
|
+
let inThen = false
|
|
34
|
+
let scenario = ""
|
|
35
|
+
for (const line of stepLines(source)) {
|
|
36
|
+
if (HEADING.test(line)) scenario = line
|
|
37
|
+
if (THEN.test(line)) inThen = true
|
|
38
|
+
else if (!CONTINUATION.test(line) && !neutral(line)) inThen = false
|
|
39
|
+
if (inThen && !neutral(line)) out.push({ scenario, line })
|
|
91
40
|
}
|
|
92
|
-
return
|
|
41
|
+
return out
|
|
93
42
|
}
|
|
94
43
|
|
|
95
|
-
export interface
|
|
44
|
+
export interface RemovedAssertions {
|
|
96
45
|
readonly path: string
|
|
97
|
-
|
|
98
|
-
readonly lines: readonly string[]
|
|
46
|
+
readonly lines: readonly Assertion[]
|
|
99
47
|
}
|
|
100
48
|
|
|
101
|
-
|
|
102
|
-
path,
|
|
103
|
-
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path))),
|
|
104
|
-
})
|
|
49
|
+
const key = (a: Assertion): string => `${a.scenario}\n${a.line}`
|
|
105
50
|
|
|
106
51
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
52
|
+
* Assertions a `.feature` file that existed before `changes` lost — the one
|
|
53
|
+
* way rewording a scenario can weaken it. Matched within each scenario and
|
|
54
|
+
* counted, so a line moving to another scenario, or one of two identical
|
|
55
|
+
* `Then`s dropped, still shows. A renamed scenario reports all of its own.
|
|
110
56
|
*/
|
|
111
|
-
export const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const text = textOf(path)
|
|
126
|
-
if (text === undefined) delete frozen.texts[path]
|
|
127
|
-
else frozen.texts[path] = text
|
|
128
|
-
}
|
|
129
|
-
frozen.at = head()
|
|
130
|
-
}
|
|
57
|
+
export const removedAssertions = (changes: readonly Change[]): readonly RemovedAssertions[] =>
|
|
58
|
+
changes
|
|
59
|
+
.filter((c) => c.path.endsWith(".feature") && c.before !== undefined)
|
|
60
|
+
.map((c) => {
|
|
61
|
+
const left = new Map<string, number>()
|
|
62
|
+
for (const a of assertionLines(c.after ?? "")) left.set(key(a), (left.get(key(a)) ?? 0) + 1)
|
|
63
|
+
const lines = assertionLines(c.before ?? "").filter((a) => {
|
|
64
|
+
const n = left.get(key(a)) ?? 0
|
|
65
|
+
left.set(key(a), n - 1)
|
|
66
|
+
return n <= 0
|
|
67
|
+
})
|
|
68
|
+
return { path: c.path, lines }
|
|
69
|
+
})
|
|
70
|
+
.filter((r) => r.lines.length > 0)
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
} from "../../flows/index.js"
|
|
11
11
|
import { gate } from "../shared/health.js"
|
|
12
12
|
import { shipped } from "../shared/options.js"
|
|
13
|
-
import type { FrozenScenarios } from "../shared/scenarios.js"
|
|
14
13
|
import { FEEDBACK, REVIEW } from "../shared/steps.js"
|
|
15
14
|
import { refusePendingHandOff } from "../shared/tail.js"
|
|
16
15
|
import * as t from "../shared/text.js"
|
|
@@ -53,13 +52,11 @@ export const reUnwind = async (
|
|
|
53
52
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
54
53
|
export const planAndBuild = async (firstBase: string): Promise<void> => {
|
|
55
54
|
let base = firstBase
|
|
56
|
-
let carried: FrozenScenarios | undefined
|
|
57
55
|
for (;;) {
|
|
58
56
|
await design(base)
|
|
59
57
|
await architecture()
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const outcome = await buildTail(false, base, built)
|
|
58
|
+
await packages()
|
|
59
|
+
const outcome = await buildTail(false, base, true)
|
|
63
60
|
if (outcome.verdict === "signoff") return
|
|
64
61
|
await reUnwind(outcome)
|
|
65
62
|
base = outcome.base
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest"
|
|
2
2
|
import { installContext, type Change, type StepRequest } from "../../flows/index.js"
|
|
3
|
-
import { declaredTests, packages
|
|
3
|
+
import { declaredTests, packages } from "./packages.js"
|
|
4
4
|
import { fixtureContext } from "../shared/text.fixture.js"
|
|
5
5
|
|
|
6
6
|
afterEach(() => installContext(undefined))
|
|
@@ -41,51 +41,34 @@ describe("declaredTests", () => {
|
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
describe("packages
|
|
45
|
-
it("
|
|
46
|
-
const queue = [
|
|
47
|
-
const
|
|
48
|
-
let steps = 0
|
|
49
|
-
const touch = (path: string, status: Change["status"]): void => {
|
|
50
|
-
touches.push({ at: steps, change: { path, status, before: undefined, after: "x" } })
|
|
51
|
-
}
|
|
52
|
-
const effects: Record<string, () => void> = {
|
|
53
|
-
building: () => {
|
|
54
|
-
if (queue[0] === SCENARIO_PACKAGE) {
|
|
55
|
-
touch("tests/e2e/a.feature", "added")
|
|
56
|
-
touch(".gtd/packages/00-e2e-scenarios.md", "modified")
|
|
57
|
-
} else {
|
|
58
|
-
touch("src/impl.ts", "added")
|
|
59
|
-
touch("tests/e2e/b.feature", "added")
|
|
60
|
-
touch("tests/e2e/a.feature", "modified")
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
closing: () => void queue.shift(),
|
|
64
|
-
}
|
|
44
|
+
describe("packages", () => {
|
|
45
|
+
it("closes every queued package in name order, the plan with the last", async () => {
|
|
46
|
+
const queue = [".gtd/packages/02-b.md", ".gtd/packages/01-a.md"]
|
|
47
|
+
const closings: string[] = []
|
|
65
48
|
installContext(
|
|
66
49
|
fixtureContext(
|
|
67
50
|
{},
|
|
68
51
|
{
|
|
69
52
|
step: (request: StepRequest) => {
|
|
70
|
-
|
|
71
|
-
|
|
53
|
+
if (request.kind === "run" && request.name === "closing") {
|
|
54
|
+
closings.push(request.body)
|
|
55
|
+
queue.splice(queue.indexOf([...queue].sort()[0]!), 1)
|
|
56
|
+
}
|
|
72
57
|
return Promise.resolve()
|
|
73
58
|
},
|
|
74
59
|
pushScope: () => undefined,
|
|
75
60
|
popScope: () => undefined,
|
|
76
|
-
refuse: (message: string): never => {
|
|
77
|
-
throw new Error(message)
|
|
78
|
-
},
|
|
79
61
|
glob: () => [...queue],
|
|
80
|
-
changesSince: (
|
|
81
|
-
|
|
82
|
-
head: () => `c${steps}`,
|
|
62
|
+
changesSince: (): readonly Change[] => [],
|
|
63
|
+
head: () => "c",
|
|
83
64
|
},
|
|
84
65
|
),
|
|
85
66
|
)
|
|
86
|
-
|
|
87
|
-
expect(
|
|
88
|
-
expect(
|
|
89
|
-
expect(
|
|
67
|
+
await packages()
|
|
68
|
+
expect(closings).toHaveLength(2)
|
|
69
|
+
expect(closings[0]).toContain("01-a.md")
|
|
70
|
+
expect(closings[0]).not.toContain("ARCHITECTURE")
|
|
71
|
+
expect(closings[1]).toContain("02-b.md")
|
|
72
|
+
expect(closings[1]).toContain(".gtd/ARCHITECTURE.md")
|
|
90
73
|
})
|
|
91
74
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
changesSince,
|
|
3
|
-
type Change,
|
|
4
3
|
glob,
|
|
5
4
|
head,
|
|
6
5
|
read,
|
|
@@ -10,12 +9,9 @@ import {
|
|
|
10
9
|
scope,
|
|
11
10
|
} from "../../flows/index.js"
|
|
12
11
|
import { healthy } from "../shared/health.js"
|
|
13
|
-
import {
|
|
12
|
+
import { ARCHITECTURE } from "../shared/steps.js"
|
|
14
13
|
import { build, fixSuite } from "./steps.js"
|
|
15
14
|
|
|
16
|
-
/** Package 0: the e2e scenarios, written red. */
|
|
17
|
-
export const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md"
|
|
18
|
-
|
|
19
15
|
export interface DeclaredTest {
|
|
20
16
|
readonly level: "unit" | "e2e"
|
|
21
17
|
readonly path: string
|
|
@@ -62,68 +58,33 @@ export const requireDeclaredTests = (pkg: string, since: string): void => {
|
|
|
62
58
|
)
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export interface BuiltPlan {
|
|
72
|
-
readonly ranges: readonly PackageRange[]
|
|
73
|
-
/** Paths package 0 added / modified outside `.gtd/`. */
|
|
74
|
-
readonly scenarios: { readonly added: readonly string[]; readonly changed: readonly string[] }
|
|
75
|
-
/** The wording snapshot after this run, to guard the tail and later laps. */
|
|
76
|
-
readonly frozen: FrozenScenarios | undefined
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
|
|
80
|
-
export const packageItem = async (pkg: string, frozen?: FrozenScenarios): Promise<PackageRange> => {
|
|
61
|
+
/**
|
|
62
|
+
* Build `pkg`, check its declared tests, keep the full suite green, and close
|
|
63
|
+
* it out, which removes it — and `.gtd/ARCHITECTURE.md` with the last one.
|
|
64
|
+
*/
|
|
65
|
+
export const packageItem = async (pkg: string): Promise<void> => {
|
|
81
66
|
const from = head()
|
|
82
67
|
// Rechecked after every fix too: a fix deleting a red declared test would
|
|
83
|
-
// otherwise green the suite.
|
|
68
|
+
// otherwise green the suite.
|
|
84
69
|
const declared = (turn: () => Promise<void>) => async () => {
|
|
85
70
|
await turn()
|
|
86
71
|
requireDeclaredTests(pkg, from)
|
|
87
72
|
}
|
|
88
|
-
await
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" })
|
|
93
|
-
await run("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), {
|
|
73
|
+
await declared(() => build(pkg))()
|
|
74
|
+
await healthy(declared(fixSuite))
|
|
75
|
+
const last = glob(".gtd/packages/*.md").length === 1
|
|
76
|
+
await run("closing", removeScript([pkg, ".gtd/SATISFIED.md", ...(last ? [ARCHITECTURE] : [])]), {
|
|
94
77
|
label: "Closing out the package",
|
|
95
78
|
})
|
|
96
|
-
return { pkg, from, to: head() }
|
|
97
79
|
}
|
|
98
80
|
|
|
99
81
|
/** The first queued package — the one a package step works on. */
|
|
100
82
|
export const nextPackage = (): string | undefined => [...glob(".gtd/packages/*.md")].sort()[0]
|
|
101
83
|
|
|
102
|
-
const outsideGtd = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
|
|
103
|
-
|
|
104
84
|
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
105
|
-
export const packages = (
|
|
85
|
+
export const packages = (): Promise<void> =>
|
|
106
86
|
scope("packages", async () => {
|
|
107
|
-
let frozen = carried
|
|
108
|
-
const ranges: PackageRange[] = []
|
|
109
|
-
// Captured right after package 0: a later diff to the tree would also hold what 01…N touch.
|
|
110
|
-
let touched: readonly Change[] = []
|
|
111
87
|
for (let pkg = nextPackage(); pkg !== undefined; pkg = nextPackage()) {
|
|
112
|
-
|
|
113
|
-
packageItem(pkg, pkg === SCENARIO_PACKAGE ? undefined : frozen),
|
|
114
|
-
)
|
|
115
|
-
ranges.push(range)
|
|
116
|
-
if (pkg === SCENARIO_PACKAGE) {
|
|
117
|
-
touched = changesSince(range.from).filter(
|
|
118
|
-
(c) => outsideGtd(c.path) && c.status !== "deleted",
|
|
119
|
-
)
|
|
120
|
-
frozen = freezeScenarios(
|
|
121
|
-
touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path),
|
|
122
|
-
frozen,
|
|
123
|
-
)
|
|
124
|
-
}
|
|
88
|
+
await scope("item", () => packageItem(pkg))
|
|
125
89
|
}
|
|
126
|
-
const paths = (status: string): string[] =>
|
|
127
|
-
touched.filter((c) => c.status === status).map((c) => c.path)
|
|
128
|
-
return { ranges, scenarios: { added: paths("added"), changed: paths("modified") }, frozen }
|
|
129
90
|
})
|
|
@@ -54,7 +54,7 @@ export const architecture = (): Promise<void> =>
|
|
|
54
54
|
}
|
|
55
55
|
if (changes(ARCHITECTURE).some((c) => c.status === "deleted")) {
|
|
56
56
|
refuse(
|
|
57
|
-
"gtd land: decompose: keep .gtd/ARCHITECTURE.md — the
|
|
57
|
+
"gtd land: decompose: keep .gtd/ARCHITECTURE.md — the packages are built from it; it is removed with the last package",
|
|
58
58
|
)
|
|
59
59
|
}
|
|
60
60
|
})
|