peaks-loop 4.1.0 → 4.1.1
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/CHANGELOG.md +25 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/_register.js +2 -2
- package/dist/cli/commands/code-mode-gate-should-pause-command.js +1 -1
- package/dist/cli/commands/comments-commands.d.ts +14 -0
- package/dist/cli/commands/comments-commands.js +96 -0
- package/dist/cli/commands/core/memory-command.js +10 -0
- package/dist/cli/commands/core/skill-command.js +1 -1
- package/dist/cli/commands/core/standards-command.js +1 -1
- package/dist/cli/commands/ecc-commands.d.ts +17 -22
- package/dist/cli/commands/ecc-commands.js +38 -26
- package/dist/cli/commands/prd-commands.js +8 -2
- package/dist/cli/program.js +4 -9
- package/dist/services/code/mode-gate-types.d.ts +1 -1
- package/dist/services/code/mode-gate-types.js +0 -1
- package/dist/services/code/mode-gate.js +1 -9
- package/dist/services/code/user-touchpoint-classifier.js +0 -7
- package/dist/services/code-review/ecc-bridge.d.ts +6 -6
- package/dist/services/comments/citation-rules.d.ts +154 -0
- package/dist/services/comments/citation-rules.js +241 -0
- package/dist/services/comments/comment-audit.d.ts +57 -0
- package/dist/services/comments/comment-audit.js +100 -0
- package/dist/services/comments/comment-citations.d.ts +60 -0
- package/dist/services/comments/comment-citations.js +186 -0
- package/dist/services/comments/comment-hygiene.d.ts +79 -0
- package/dist/services/comments/comment-hygiene.js +133 -0
- package/dist/services/comments/comment-prune.d.ts +88 -0
- package/dist/services/comments/comment-prune.js +148 -0
- package/dist/services/comments/prune-apply.d.ts +60 -0
- package/dist/services/comments/prune-apply.js +150 -0
- package/dist/services/comments/repo-path-probe.d.ts +43 -0
- package/dist/services/comments/repo-path-probe.js +78 -0
- package/dist/services/log/retention.d.ts +0 -16
- package/dist/services/log/retention.js +0 -17
- package/dist/services/memory/project-memory-service/index.d.ts +1 -1
- package/dist/services/memory/project-memory-service/index.js +1 -1
- package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +19 -7
- package/dist/services/memory/project-memory-service/store/atomic-write.js +120 -26
- package/dist/services/prd/handoff-frontmatter.js +61 -0
- package/dist/services/prd/handoff-gate-evidence.js +14 -10
- package/dist/services/prd/handoff-service.d.ts +11 -1
- package/dist/services/prd/handoff-service.js +11 -1
- package/dist/services/prd/handoff-types.d.ts +33 -1
- package/dist/services/recommendations/installed-capability-detector.d.ts +5 -5
- package/dist/services/recommendations/installed-capability-detector.js +11 -10
- package/dist/services/scan/archetype-detection.d.ts +37 -0
- package/dist/services/scan/archetype-detection.js +175 -2
- package/dist/services/scan/archetype-service.js +36 -22
- package/dist/services/scan/scan-types.d.ts +9 -0
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +2 -2
- package/dist/services/workspace/generated-artifacts-stamp.js +2 -2
- package/dist/services/workspace/workspace-service.js +1 -1
- package/package.json +5 -5
- package/scripts/install-skills.mjs +0 -177
- package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +3 -1
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +3 -2
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +29 -7
- package/skills/peaks-code/references/frontend-only-mode.md +2 -2
- package/skills/peaks-code/references/startup-sequence.md +0 -4
- package/dist/cli/commands/upgrade-commands.d.ts +0 -25
- package/dist/cli/commands/upgrade-commands.js +0 -154
- package/dist/services/upgrade/1x-detector-service.d.ts +0 -7
- package/dist/services/upgrade/1x-detector-service.js +0 -96
- package/dist/services/upgrade/gitignore-migrate-service.d.ts +0 -56
- package/dist/services/upgrade/gitignore-migrate-service.js +0 -170
- package/dist/services/upgrade/upgrade-service.d.ts +0 -81
- package/dist/services/upgrade/upgrade-service.js +0 -428
- package/skills/peaks-code/references/step-0-55-1x-detection.md +0 -83
|
@@ -48,6 +48,20 @@ function blockSequence(key, values) {
|
|
|
48
48
|
return [`${key}: []`];
|
|
49
49
|
return [`${key}:`, ...values.map((value) => ` - ${yamlScalar(value)}`)];
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The OPTIONAL form: no line at all when the field is absent or empty.
|
|
53
|
+
*
|
|
54
|
+
* `goals` and its siblings cannot use this — `isHandoffFrontmatter` requires an
|
|
55
|
+
* ARRAY there, so an omitted line would fail the shape check on read, and `[]` is
|
|
56
|
+
* the honest rendering of "declared, none". The authored fields have no such
|
|
57
|
+
* requirement: a capsule that names no files declares nothing, and a `files: []`
|
|
58
|
+
* line would read as a claim that the slice touches no file.
|
|
59
|
+
*/
|
|
60
|
+
function blockOptionalSequence(key, values) {
|
|
61
|
+
if (values === undefined || values.length === 0)
|
|
62
|
+
return [];
|
|
63
|
+
return [`${key}:`, ...values.map((value) => ` - ${yamlScalar(value)}`)];
|
|
64
|
+
}
|
|
51
65
|
/**
|
|
52
66
|
* Render `gateEvidence` as a YAML map of quoted path scalars, or as NOTHING.
|
|
53
67
|
*
|
|
@@ -103,6 +117,44 @@ function gateEvidenceBlock(evidence) {
|
|
|
103
117
|
});
|
|
104
118
|
return entries.length === 0 ? [] : ['gateEvidence:', ...entries];
|
|
105
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Render a sequence of records (`decisions`, `risks`) as a YAML block list, or
|
|
122
|
+
* as NOTHING when the list is absent or empty — the same omission policy as
|
|
123
|
+
* `gateEvidence`, so a capsule that declares no risks gains no noise line.
|
|
124
|
+
*
|
|
125
|
+
* The field names are rendered in the order given, and every value is validated
|
|
126
|
+
* as a string HERE rather than dropped later. That is deliberate: the bug these
|
|
127
|
+
* fields fix was a serializer that rendered from the type and so discarded
|
|
128
|
+
* anything the type did not name, silently, on every rewrite. A record missing a
|
|
129
|
+
* required field therefore has to stop the write, not vanish from it.
|
|
130
|
+
*/
|
|
131
|
+
function blockRecords(key, items, requiredFields) {
|
|
132
|
+
if (items === undefined || items.length === 0)
|
|
133
|
+
return [];
|
|
134
|
+
const lines = [`${key}:`];
|
|
135
|
+
items.forEach((rawItem, index) => {
|
|
136
|
+
// The values are checked at RUNTIME, not trusted from the type, because a
|
|
137
|
+
// capsule reaches here from `readHandoff` as well as from a producer: the
|
|
138
|
+
// spread that makes an authored field survive a read also makes it survive
|
|
139
|
+
// unvalidated. A record missing a required field stops the write rather than
|
|
140
|
+
// vanishing from it — which is precisely how the old serializer made a
|
|
141
|
+
// declared risk look satisfied.
|
|
142
|
+
const record = rawItem !== null && typeof rawItem === 'object' ? { ...rawItem } : {};
|
|
143
|
+
const missing = requiredFields.filter((field) => typeof record[field] !== 'string' || record[field] === '');
|
|
144
|
+
if (missing.length > 0) {
|
|
145
|
+
throw new Error(`handoff: ${key}[${index}] is missing ${missing.join(', ')} — refusing to write a ` +
|
|
146
|
+
`record the next role cannot read (a risk without a mitigation is a red line, and ` +
|
|
147
|
+
'dropping the field is how it looked satisfied)');
|
|
148
|
+
}
|
|
149
|
+
requiredFields.forEach((field, fieldIndex) => {
|
|
150
|
+
const value = record[field];
|
|
151
|
+
lines.push(fieldIndex === 0
|
|
152
|
+
? ` - ${field}: ${yamlScalar(value)}`
|
|
153
|
+
: ` ${field}: ${yamlScalar(value)}`);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
106
158
|
/**
|
|
107
159
|
* Serialize `frontmatter` into the fenced block, terminated by the closing
|
|
108
160
|
* `---` and a trailing newline. Callers append the body verbatim, which keeps
|
|
@@ -125,6 +177,15 @@ export function serializeHandoffFrontmatter(frontmatter) {
|
|
|
125
177
|
...blockSequence('acceptanceCriteria', frontmatter.acceptanceCriteria),
|
|
126
178
|
...blockSequence('preservedBehavior', frontmatter.preservedBehavior),
|
|
127
179
|
`handoffPath: ${yamlScalar(frontmatter.handoffPath)}`,
|
|
180
|
+
// The authored fields: written by peaks-prd / peaks-rd, read by peaks-qa's
|
|
181
|
+
// mechanical cross-checks, and PRESERVED by every rewrite. They sit after
|
|
182
|
+
// `handoffPath` because `schemaVersion` and `sha256` must stay first-parsed,
|
|
183
|
+
// and before `gateEvidence` because that block is last by contract.
|
|
184
|
+
...blockOptionalSequence('scope', frontmatter.scope),
|
|
185
|
+
...blockOptionalSequence('files', frontmatter.files),
|
|
186
|
+
...blockRecords('decisions', frontmatter.decisions, ['id', 'summary', 'rationale']),
|
|
187
|
+
...blockRecords('risks', frontmatter.risks, ['id', 'description', 'mitigation']),
|
|
188
|
+
...blockOptionalSequence('nextActions', frontmatter.nextActions),
|
|
128
189
|
// LAST, after every anchored field: `schemaVersion` / `sha256` are
|
|
129
190
|
// matched as `^`-anchored lines by the gate and both audit loaders, so
|
|
130
191
|
// nothing new may be inserted before them. A nested map also renders
|
|
@@ -8,16 +8,8 @@
|
|
|
8
8
|
// `skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:35-41`; the
|
|
9
9
|
// key set is `GATE_EVIDENCE_KEYS` in `./handoff-types.js`.
|
|
10
10
|
//
|
|
11
|
-
// WHAT THIS FILE USED TO SAY, AND WHY THAT WAS FALSE: its header claimed the
|
|
12
|
-
// field was a `string[]` of "gate names" and that `initHandoff` wrote it.
|
|
13
|
-
// Neither held. `grep -rn gateEvidence src/` hit this file and nothing else —
|
|
14
|
-
// there was no producer (`HandoffFrontmatter` had no such field,
|
|
15
|
-
// `serializeHandoffFrontmatter` never emitted it, `initHandoff` rejected it)
|
|
16
|
-
// and no consumer. The field was prose describing data that did not exist,
|
|
17
|
-
// and the comment asserting a producer was the reason nobody noticed.
|
|
18
|
-
//
|
|
19
11
|
// WHAT IS ON DISK NOW, stated precisely because a header that overclaims is
|
|
20
|
-
// the defect
|
|
12
|
+
// the defect below. B1 added the producer FUNCTIONS; B2 wired their callers,
|
|
21
13
|
// which is what made the difference — until then every producer passed
|
|
22
14
|
// nothing, so no capsule carried the field and saying otherwise would have
|
|
23
15
|
// described a fact that held only inside tests (F1 of `rid-b1-qa`). At
|
|
@@ -28,7 +20,19 @@
|
|
|
28
20
|
// artifact is readable — and carries no `gateEvidence` block at all when it
|
|
29
21
|
// is not. Gate C (`checkPrerequisites` at `rd:qa-handoff`) fails a declared
|
|
30
22
|
// path that is not on disk. Every clause above is asserted end-to-end in
|
|
31
|
-
// `tests/unit/prd/gate-evidence-derivation.test.ts`.
|
|
23
|
+
// `tests/unit/prd/gate-evidence-derivation.test.ts`. The derived map is also
|
|
24
|
+
// printed by `peaks prd handoff init` (dry-run and applied), so the key set
|
|
25
|
+
// does not have to be read out of this source file.
|
|
26
|
+
//
|
|
27
|
+
// WHAT THIS FILE USED TO SAY, AND WHY THAT WAS FALSE: its header claimed the
|
|
28
|
+
// field was a `string[]` of "gate names" and that `initHandoff` wrote it.
|
|
29
|
+
// Neither held. `grep -rn gateEvidence src/` hit this file and nothing else —
|
|
30
|
+
// there was no producer (`HandoffFrontmatter` had no such field,
|
|
31
|
+
// `serializeHandoffFrontmatter` never emitted it, `initHandoff` rejected it)
|
|
32
|
+
// and no consumer. The field was prose describing data that did not exist,
|
|
33
|
+
// and the comment asserting a producer was the reason nobody noticed. That
|
|
34
|
+
// paragraph describes the PRE-B1 state; reading it as the current state is
|
|
35
|
+
// the exact misreading that put it here.
|
|
32
36
|
//
|
|
33
37
|
// WHY IT IS STILL STAND-ALONE: keeping the typed-value logic here, rather
|
|
34
38
|
// than in `handoff-service.ts`, keeps `HandoffFrontmatter` a pure data shape
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* the body content as UTF-8 bytes. The body MUST be the literal
|
|
23
23
|
* markdown source — no normalization, no trailing-newline padding.
|
|
24
24
|
*/
|
|
25
|
-
import type { GateEvidence, Handoff, HandoffProbe } from './handoff-types.js';
|
|
25
|
+
import type { GateEvidence, Handoff, HandoffDecision, HandoffProbe, HandoffRisk } from './handoff-types.js';
|
|
26
26
|
export { handoffRelativePath, resolveHandoffPath } from './handoff-path-resolution.js';
|
|
27
27
|
/** Compute the lowercase hex sha256 of a UTF-8 string. */
|
|
28
28
|
export declare function sha256OfBody(body: string): string;
|
|
@@ -58,6 +58,16 @@ export declare function initHandoff(opts: {
|
|
|
58
58
|
* is refused here by a throw and there by a status; neither silently
|
|
59
59
|
* produces a map the other cannot. */
|
|
60
60
|
gateEvidence?: GateEvidence;
|
|
61
|
+
/** The authored fields peaks-qa cross-checks (`decisions[] ↔ tests`,
|
|
62
|
+
* `risks[] ↔ security tests`, `files[] ↔ git diff`). They are OPTIONAL and
|
|
63
|
+
* pass through verbatim: unlike `gateEvidence`, nothing derives them — they
|
|
64
|
+
* are a writer's judgement about the slice, so a producer that has not made
|
|
65
|
+
* that judgement declares nothing rather than an empty claim. */
|
|
66
|
+
scope?: readonly string[];
|
|
67
|
+
files?: readonly string[];
|
|
68
|
+
decisions?: readonly HandoffDecision[];
|
|
69
|
+
risks?: readonly HandoffRisk[];
|
|
70
|
+
nextActions?: readonly string[];
|
|
61
71
|
}): Handoff;
|
|
62
72
|
/** Write a Handoff to disk. `projectRoot` is the absolute project
|
|
63
73
|
* root (so the `.peaks/_runtime/...` path is resolved absolutely).
|
|
@@ -63,7 +63,17 @@ export function initHandoff(opts) {
|
|
|
63
63
|
// would also make `serializeHandoffFrontmatter`'s
|
|
64
64
|
// `frontmatter.gateEvidence` key present-but-undefined for every caller
|
|
65
65
|
// that declares nothing.
|
|
66
|
-
...(opts.gateEvidence === undefined ? {} : { gateEvidence: opts.gateEvidence })
|
|
66
|
+
...(opts.gateEvidence === undefined ? {} : { gateEvidence: opts.gateEvidence }),
|
|
67
|
+
// The authored fields, each spread only when present. The same
|
|
68
|
+
// `exactOptionalPropertyTypes` reason as `gateEvidence` above: an absent
|
|
69
|
+
// field is absent, not present-and-undefined, so a capsule that declares
|
|
70
|
+
// nothing writes nothing — and a copy is taken so a caller mutating its own
|
|
71
|
+
// array afterwards cannot change a handoff that has already been hashed.
|
|
72
|
+
...(opts.scope === undefined ? {} : { scope: [...opts.scope] }),
|
|
73
|
+
...(opts.files === undefined ? {} : { files: [...opts.files] }),
|
|
74
|
+
...(opts.decisions === undefined ? {} : { decisions: [...opts.decisions] }),
|
|
75
|
+
...(opts.risks === undefined ? {} : { risks: [...opts.risks] }),
|
|
76
|
+
...(opts.nextActions === undefined ? {} : { nextActions: [...opts.nextActions] })
|
|
67
77
|
};
|
|
68
78
|
return { frontmatter, body: opts.body };
|
|
69
79
|
}
|
|
@@ -63,7 +63,39 @@ export type GateEvidence = Readonly<Partial<Record<GateEvidenceKey, string>>>;
|
|
|
63
63
|
* immutability: once written, peaks-rd / peaks-qa / sub-agents MUST
|
|
64
64
|
* NOT mutate the on-disk copy.
|
|
65
65
|
*/
|
|
66
|
-
|
|
66
|
+
/** One entry of `decisions[]` — a choice an implementer could question. */
|
|
67
|
+
export type HandoffDecision = {
|
|
68
|
+
readonly id: string;
|
|
69
|
+
readonly summary: string;
|
|
70
|
+
readonly rationale: string;
|
|
71
|
+
};
|
|
72
|
+
/** One entry of `risks[]`. `mitigation` is required by the field rule in
|
|
73
|
+
* `skills/bee/peaks-rd/references/writing-handoff-frontmatter.md:52`: a risk
|
|
74
|
+
* without one is a red line, so the type has no optional form of it. */
|
|
75
|
+
export type HandoffRisk = {
|
|
76
|
+
readonly id: string;
|
|
77
|
+
readonly description: string;
|
|
78
|
+
readonly mitigation: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The fields a WRITER authors and a READER cross-checks, absent when the capsule
|
|
82
|
+
* declares nothing. They were documented as required frontmatter long before the
|
|
83
|
+
* type carried them — `readHandoff` spread the parsed YAML, so an authored capsule
|
|
84
|
+
* kept them at runtime while the serializer, which renders from the TYPE, dropped
|
|
85
|
+
* them on every rewrite. That is the data loss `handoff-auto-regen` used to cause
|
|
86
|
+
* and these fields exist to end.
|
|
87
|
+
*/
|
|
88
|
+
export type HandoffAuthoredFields = {
|
|
89
|
+
/** Repo-relative paths this slice touches, sorted. */
|
|
90
|
+
readonly scope?: readonly string[];
|
|
91
|
+
/** Files the slice expects in its diff; peaks-qa checks this against git. */
|
|
92
|
+
readonly files?: readonly string[];
|
|
93
|
+
readonly decisions?: readonly HandoffDecision[];
|
|
94
|
+
readonly risks?: readonly HandoffRisk[];
|
|
95
|
+
/** Verb-first; what the next role does, in order. */
|
|
96
|
+
readonly nextActions?: readonly string[];
|
|
97
|
+
};
|
|
98
|
+
export interface HandoffFrontmatter extends HandoffAuthoredFields {
|
|
67
99
|
readonly requestId: string;
|
|
68
100
|
readonly sessionId: string;
|
|
69
101
|
readonly schemaVersion: HandoffSchemaVersion;
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* the question from evidence that already exists on disk — no install
|
|
10
10
|
* side effect, no network, no subprocess:
|
|
11
11
|
*
|
|
12
|
-
* - `everything-claude-code.*` — satisfied when the ECC
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* - `everything-claude-code.*` — satisfied when the ECC agents are readable,
|
|
13
|
+
* i.e. `~/.peaks/agents/ecc/` holds at least one agent (landed by
|
|
14
|
+
* `peaks ecc install` from the `ecc-universal` dependency).
|
|
15
15
|
* - a source that maps to an npm package — satisfied when that package
|
|
16
16
|
* (or its `.bin` shim) is present in the project's `node_modules`.
|
|
17
17
|
*
|
|
@@ -23,8 +23,8 @@ export type InstalledCapabilityProbe = {
|
|
|
23
23
|
readonly projectRoot: string;
|
|
24
24
|
/** Override for tests; defaults to `<projectRoot>/node_modules`. */
|
|
25
25
|
readonly nodeModulesDir?: string;
|
|
26
|
-
/** Override for tests; defaults to a live
|
|
27
|
-
readonly
|
|
26
|
+
/** Override for tests; defaults to a live probe of the materialized copy. */
|
|
27
|
+
readonly eccAgentsAvailable?: boolean;
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
30
|
* Return the capabilityIds whose acquisition is already satisfied.
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* the question from evidence that already exists on disk — no install
|
|
10
10
|
* side effect, no network, no subprocess:
|
|
11
11
|
*
|
|
12
|
-
* - `everything-claude-code.*` — satisfied when the ECC
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* - `everything-claude-code.*` — satisfied when the ECC agents are readable,
|
|
13
|
+
* i.e. `~/.peaks/agents/ecc/` holds at least one agent (landed by
|
|
14
|
+
* `peaks ecc install` from the `ecc-universal` dependency).
|
|
15
15
|
* - a source that maps to an npm package — satisfied when that package
|
|
16
16
|
* (or its `.bin` shim) is present in the project's `node_modules`.
|
|
17
17
|
*
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync } from 'node:fs';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
|
-
import { hasMaterializedEccAgents
|
|
23
|
+
import { hasMaterializedEccAgents } from 'peaks-loop-mut';
|
|
24
24
|
import { seedCapabilityItems } from './capability-seed-items.js';
|
|
25
25
|
const ECC_SOURCE_ID = 'everything-claude-code';
|
|
26
26
|
/**
|
|
@@ -42,12 +42,13 @@ function packagePresent(nodeModulesDir, packageName) {
|
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
function
|
|
45
|
+
function eccAgentsAvailable() {
|
|
46
46
|
try {
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// `~/.peaks/
|
|
50
|
-
|
|
47
|
+
// The materialized copy is what the LLM can dispatch, so it is the only
|
|
48
|
+
// evidence that counts. The cache probe this replaced
|
|
49
|
+
// (`listCachedAgents()`, reading `~/.peaks/cache/ecc-<sha>/`) described
|
|
50
|
+
// something downloaded but not necessarily usable.
|
|
51
|
+
return hasMaterializedEccAgents();
|
|
51
52
|
}
|
|
52
53
|
catch {
|
|
53
54
|
return false;
|
|
@@ -59,7 +60,7 @@ function eccCacheAvailable() {
|
|
|
59
60
|
*/
|
|
60
61
|
export function detectInstalledCapabilityIds(probe) {
|
|
61
62
|
const nodeModulesDir = probe.nodeModulesDir ?? join(probe.projectRoot, 'node_modules');
|
|
62
|
-
const eccAvailable = probe.
|
|
63
|
+
const eccAvailable = probe.eccAgentsAvailable ?? eccAgentsAvailable();
|
|
63
64
|
const installed = new Set();
|
|
64
65
|
for (const item of seedCapabilityItems) {
|
|
65
66
|
if (item.sourceId === ECC_SOURCE_ID) {
|
|
@@ -30,6 +30,43 @@ export declare function readPackageJsonDeps(projectRoot: string): Promise<{
|
|
|
30
30
|
deps: Record<string, string>;
|
|
31
31
|
}>;
|
|
32
32
|
export declare function detectBackendFrameworks(deps: Record<string, string>): Promise<string[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Manifests that only a service writes. Language-diverse on purpose: every
|
|
35
|
+
* other probe in this file is Node-specific, and a repository whose backend is
|
|
36
|
+
* Go / Java / Kotlin / Python / Ruby / PHP / Rust has no Node manifest at all,
|
|
37
|
+
* so a Node-only evidence set reported it as `frontendOnly`.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SERVICE_ONLY_MANIFEST_FILES: string[];
|
|
40
|
+
/**
|
|
41
|
+
* Manifests a service MAY write and a library or CLI also writes. Counted as
|
|
42
|
+
* backend evidence only when the file itself names a web framework — the
|
|
43
|
+
* alternative is a boolean that fires on any Python or Rust directory, which
|
|
44
|
+
* trades one wrong answer for another.
|
|
45
|
+
*/
|
|
46
|
+
export declare const CONDITIONAL_SERVICE_MANIFESTS: ReadonlyArray<readonly [string, RegExp]>;
|
|
47
|
+
/**
|
|
48
|
+
* How deep from the project root a service is allowed to sit before its
|
|
49
|
+
* manifest stops being counted. Two levels covers the layouts that were
|
|
50
|
+
* invisible (`apps/gateway/package.json`, `services/checkout/requirements.txt`,
|
|
51
|
+
* `web/` + `cmd/server`), and a deeper walk would read generated trees.
|
|
52
|
+
*/
|
|
53
|
+
export declare const SERVICE_MANIFEST_MAX_DEPTH = 2;
|
|
54
|
+
/**
|
|
55
|
+
* Backend evidence that lives OUTSIDE the root manifest: a workspace package's
|
|
56
|
+
* own `package.json`, and any non-Node service manifest.
|
|
57
|
+
*
|
|
58
|
+
* Returns `dir: evidence` strings (`apps/gateway: express`, `go.mod`,
|
|
59
|
+
* `services/checkout: requirements.txt`) rather than a boolean, because the
|
|
60
|
+
* whole defect this probe closes was a verdict that could not say what it saw.
|
|
61
|
+
*/
|
|
62
|
+
export declare function detectNestedServiceEvidence(projectRoot: string, backendDepNames?: readonly string[]): Promise<string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Next.js server actions: `'use server'` in a file under `app/`. Route handlers
|
|
65
|
+
* under `pages/api` / `app/api` were the only Next backend the detector knew
|
|
66
|
+
* about, so a Next app that mutates data through actions — the documented
|
|
67
|
+
* Next.js way — was reported as having no backend at all.
|
|
68
|
+
*/
|
|
69
|
+
export declare function detectNextServerActions(projectRoot: string): Promise<boolean>;
|
|
33
70
|
export declare function detectNextApiRoutes(projectRoot: string, hasNext: boolean): Promise<boolean>;
|
|
34
71
|
export declare function detectBackendDirs(projectRoot: string): Promise<string[]>;
|
|
35
72
|
export declare function detectSwagger(projectRoot: string): Promise<string[]>;
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* `decideIntegrationMode`) and the public `scanArchetype` entry; the
|
|
11
11
|
* `frontendOnly` / `frontendOnlyReason` output contract is unchanged.
|
|
12
12
|
*/
|
|
13
|
-
import { stat } from 'node:fs/promises';
|
|
14
|
-
import { join } from 'node:path';
|
|
13
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
14
|
+
import { join, relative as relativePath } from 'node:path';
|
|
15
15
|
import { isDirectory, pathExists, readText } from 'peaks-loop-shared/fs';
|
|
16
16
|
export const BACKEND_DEP_NAMES = [
|
|
17
17
|
'express',
|
|
@@ -85,6 +85,179 @@ export async function readPackageJsonDeps(projectRoot) {
|
|
|
85
85
|
export async function detectBackendFrameworks(deps) {
|
|
86
86
|
return BACKEND_DEP_NAMES.filter((name) => name !== 'next' && Object.prototype.hasOwnProperty.call(deps, name));
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Manifests that only a service writes. Language-diverse on purpose: every
|
|
90
|
+
* other probe in this file is Node-specific, and a repository whose backend is
|
|
91
|
+
* Go / Java / Kotlin / Python / Ruby / PHP / Rust has no Node manifest at all,
|
|
92
|
+
* so a Node-only evidence set reported it as `frontendOnly`.
|
|
93
|
+
*/
|
|
94
|
+
export const SERVICE_ONLY_MANIFEST_FILES = [
|
|
95
|
+
'go.mod',
|
|
96
|
+
'pom.xml',
|
|
97
|
+
'build.gradle',
|
|
98
|
+
'build.gradle.kts',
|
|
99
|
+
'settings.gradle',
|
|
100
|
+
'settings.gradle.kts',
|
|
101
|
+
'manage.py',
|
|
102
|
+
'config.ru',
|
|
103
|
+
'artisan'
|
|
104
|
+
];
|
|
105
|
+
/**
|
|
106
|
+
* Manifests a service MAY write and a library or CLI also writes. Counted as
|
|
107
|
+
* backend evidence only when the file itself names a web framework — the
|
|
108
|
+
* alternative is a boolean that fires on any Python or Rust directory, which
|
|
109
|
+
* trades one wrong answer for another.
|
|
110
|
+
*/
|
|
111
|
+
export const CONDITIONAL_SERVICE_MANIFESTS = [
|
|
112
|
+
['requirements.txt', /\b(fastapi|flask|django|uvicorn|gunicorn|aiohttp|tornado|starlette)\b/i],
|
|
113
|
+
['pyproject.toml', /\b(fastapi|flask|django|uvicorn|gunicorn|aiohttp|tornado|starlette)\b/i],
|
|
114
|
+
['Cargo.toml', /\b(actix-web|axum|rocket|warp|tonic|hyper|poem)\b/i],
|
|
115
|
+
['Gemfile', /\b(rails|sinatra|hanami|puma|rack)\b/i],
|
|
116
|
+
['composer.json', /\b(laravel|symfony|slim)\b/i]
|
|
117
|
+
];
|
|
118
|
+
/** Directories never worth descending into when looking for a service manifest. */
|
|
119
|
+
const MANIFEST_WALK_SKIP_DIRS = new Set([
|
|
120
|
+
'node_modules',
|
|
121
|
+
'dist',
|
|
122
|
+
'build',
|
|
123
|
+
'out',
|
|
124
|
+
'coverage',
|
|
125
|
+
'target',
|
|
126
|
+
'vendor',
|
|
127
|
+
'__pycache__',
|
|
128
|
+
'.git'
|
|
129
|
+
]);
|
|
130
|
+
/**
|
|
131
|
+
* How deep from the project root a service is allowed to sit before its
|
|
132
|
+
* manifest stops being counted. Two levels covers the layouts that were
|
|
133
|
+
* invisible (`apps/gateway/package.json`, `services/checkout/requirements.txt`,
|
|
134
|
+
* `web/` + `cmd/server`), and a deeper walk would read generated trees.
|
|
135
|
+
*/
|
|
136
|
+
export const SERVICE_MANIFEST_MAX_DEPTH = 2;
|
|
137
|
+
/**
|
|
138
|
+
* How many leading bytes of a source file are read to find a directive. `'use
|
|
139
|
+
* server'` must be the first statement, so the head is the whole question, and
|
|
140
|
+
* reading a bounded head keeps this probe from pulling multi-megabyte files into
|
|
141
|
+
* memory.
|
|
142
|
+
*/
|
|
143
|
+
const DIRECTIVE_HEAD_BYTES = 400;
|
|
144
|
+
async function readDirSafe(dir) {
|
|
145
|
+
try {
|
|
146
|
+
return await readdir(dir, { withFileTypes: true });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// An unreadable directory is not evidence of a backend, and not of its
|
|
150
|
+
// absence either; the root manifest already carries the project.
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function relativeFromRoot(projectRoot, dir) {
|
|
155
|
+
return relativePath(projectRoot, dir).replace(/\\/g, '/');
|
|
156
|
+
}
|
|
157
|
+
function labeled(relative, file) {
|
|
158
|
+
return relative === '' ? file : `${relative}/${file}`;
|
|
159
|
+
}
|
|
160
|
+
function descendable(entries, dir, depth) {
|
|
161
|
+
if (depth >= SERVICE_MANIFEST_MAX_DEPTH)
|
|
162
|
+
return [];
|
|
163
|
+
return entries
|
|
164
|
+
.filter((entry) => entry.isDirectory() &&
|
|
165
|
+
!MANIFEST_WALK_SKIP_DIRS.has(entry.name) &&
|
|
166
|
+
!entry.name.startsWith('.'))
|
|
167
|
+
.map((entry) => ({ dir: join(dir, entry.name), depth: depth + 1 }));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Service manifests sitting in ONE directory: the unmistakable ones by name,
|
|
171
|
+
* and the shared ones only when the file names a web framework.
|
|
172
|
+
*/
|
|
173
|
+
async function manifestEvidenceInDir(dir, relative, names) {
|
|
174
|
+
const found = SERVICE_ONLY_MANIFEST_FILES.filter((manifest) => names.has(manifest)).map((manifest) => labeled(relative, manifest));
|
|
175
|
+
for (const [manifest, pattern] of CONDITIONAL_SERVICE_MANIFESTS) {
|
|
176
|
+
if (!names.has(manifest))
|
|
177
|
+
continue;
|
|
178
|
+
const text = await readText(join(dir, manifest)).catch(() => '');
|
|
179
|
+
const matched = pattern.exec(text);
|
|
180
|
+
if (matched !== null) {
|
|
181
|
+
found.push(`${labeled(relative, manifest)} (${matched[0].toLowerCase()})`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A nested `package.json` declares its own dependencies, and a workspace root
|
|
188
|
+
* does not list them. Reading only the root is how an express service under
|
|
189
|
+
* `apps/` became `frontendOnly`.
|
|
190
|
+
*/
|
|
191
|
+
async function workspacePackageEvidence(dir, relative, names, backendDepNames) {
|
|
192
|
+
if (relative === '' || !names.has('package.json'))
|
|
193
|
+
return [];
|
|
194
|
+
const { deps } = await readPackageJsonDeps(dir);
|
|
195
|
+
return backendDepNames
|
|
196
|
+
.filter((dep) => dep !== 'next' && Object.prototype.hasOwnProperty.call(deps, dep))
|
|
197
|
+
.map((dep) => `${relative}: ${dep}`);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Backend evidence that lives OUTSIDE the root manifest: a workspace package's
|
|
201
|
+
* own `package.json`, and any non-Node service manifest.
|
|
202
|
+
*
|
|
203
|
+
* Returns `dir: evidence` strings (`apps/gateway: express`, `go.mod`,
|
|
204
|
+
* `services/checkout: requirements.txt`) rather than a boolean, because the
|
|
205
|
+
* whole defect this probe closes was a verdict that could not say what it saw.
|
|
206
|
+
*/
|
|
207
|
+
export async function detectNestedServiceEvidence(projectRoot, backendDepNames = BACKEND_DEP_NAMES) {
|
|
208
|
+
const evidence = [];
|
|
209
|
+
const queue = [{ dir: projectRoot, depth: 0 }];
|
|
210
|
+
while (queue.length > 0) {
|
|
211
|
+
const current = queue.shift();
|
|
212
|
+
if (current === undefined)
|
|
213
|
+
break;
|
|
214
|
+
const entries = await readDirSafe(current.dir);
|
|
215
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
216
|
+
const relative = relativeFromRoot(projectRoot, current.dir);
|
|
217
|
+
evidence.push(...(await manifestEvidenceInDir(current.dir, relative, names)));
|
|
218
|
+
evidence.push(...(await workspacePackageEvidence(current.dir, relative, names, backendDepNames)));
|
|
219
|
+
queue.push(...descendable(entries, current.dir, current.depth));
|
|
220
|
+
}
|
|
221
|
+
return evidence.sort();
|
|
222
|
+
}
|
|
223
|
+
/** Does this directory tree contain a file opening with `'use server'`? */
|
|
224
|
+
async function hasUseServerFile(rootDir) {
|
|
225
|
+
const queue = [{ dir: rootDir, depth: 0 }];
|
|
226
|
+
while (queue.length > 0) {
|
|
227
|
+
const current = queue.shift();
|
|
228
|
+
if (current === undefined)
|
|
229
|
+
break;
|
|
230
|
+
const entries = await readDirSafe(current.dir);
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
const full = join(current.dir, entry.name);
|
|
233
|
+
if (entry.isDirectory())
|
|
234
|
+
continue;
|
|
235
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(entry.name))
|
|
236
|
+
continue;
|
|
237
|
+
const text = await readText(full).catch(() => '');
|
|
238
|
+
if (/['"]use server['"]/.test(text.slice(0, DIRECTIVE_HEAD_BYTES)))
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
queue.push(...descendable(entries, current.dir, current.depth));
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Next.js server actions: `'use server'` in a file under `app/`. Route handlers
|
|
247
|
+
* under `pages/api` / `app/api` were the only Next backend the detector knew
|
|
248
|
+
* about, so a Next app that mutates data through actions — the documented
|
|
249
|
+
* Next.js way — was reported as having no backend at all.
|
|
250
|
+
*/
|
|
251
|
+
export async function detectNextServerActions(projectRoot) {
|
|
252
|
+
for (const root of ['app', 'src/app']) {
|
|
253
|
+
const rootDir = join(projectRoot, root);
|
|
254
|
+
if (await isDirectory(rootDir)) {
|
|
255
|
+
if (await hasUseServerFile(rootDir))
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
88
261
|
export async function detectNextApiRoutes(projectRoot, hasNext) {
|
|
89
262
|
if (!hasNext) {
|
|
90
263
|
return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isDirectory } from 'peaks-loop-shared/fs';
|
|
4
|
-
import { detectBackendDirs, detectBackendFrameworks, detectMonorepoConfigs, detectNextApiRoutes, detectSwagger, GREENFIELD_MAX_SRC_FILES, GREENFIELD_MAX_LOCKFILE_DAYS, HIGH_CONFIDENCE_SIGNAL_COUNT, LEGACY_MIN_SRC_FILES, lockfileAgeDays, LOCKFILE_STALE_DAYS, readPackageJsonDeps } from './archetype-detection.js';
|
|
4
|
+
import { detectBackendDirs, detectBackendFrameworks, detectMonorepoConfigs, detectNestedServiceEvidence, detectNextApiRoutes, detectNextServerActions, detectSwagger, GREENFIELD_MAX_SRC_FILES, GREENFIELD_MAX_LOCKFILE_DAYS, HIGH_CONFIDENCE_SIGNAL_COUNT, LEGACY_MIN_SRC_FILES, lockfileAgeDays, LOCKFILE_STALE_DAYS, readPackageJsonDeps } from './archetype-detection.js';
|
|
5
5
|
async function countSrcFiles(projectRoot, max = 500) {
|
|
6
6
|
const srcDir = join(projectRoot, 'src');
|
|
7
7
|
if (!(await isDirectory(srcDir))) {
|
|
@@ -30,11 +30,26 @@ async function countSrcFiles(projectRoot, max = 500) {
|
|
|
30
30
|
}
|
|
31
31
|
return count;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* THE backend predicate — one answer, three callers.
|
|
35
|
+
*
|
|
36
|
+
* `decideArchetype`, `decideFrontendOnly` and `decideIntegrationMode` each
|
|
37
|
+
* spelled the same triple out inline, so a signal added for one of them left
|
|
38
|
+
* the other two reading a different project. The report then contradicted
|
|
39
|
+
* itself, which is the failure `54ba0cc4` ("one answer for where frontendOnly
|
|
40
|
+
* lives") was meant to end but did not. Anything that asks "does this repo
|
|
41
|
+
* have a backend?" asks here.
|
|
42
|
+
*/
|
|
43
|
+
function hasBackendEvidence(detected) {
|
|
44
|
+
return (detected.hasBackendFramework ||
|
|
45
|
+
detected.hasNextApiRoutes ||
|
|
46
|
+
detected.hasNextServerActions ||
|
|
47
|
+
detected.backendDirsPresent.length > 0 ||
|
|
48
|
+
detected.nestedServiceEvidence.length > 0);
|
|
49
|
+
}
|
|
33
50
|
function decideArchetype(detected) {
|
|
34
51
|
const signals = [];
|
|
35
|
-
const hasBackend = detected
|
|
36
|
-
detected.hasNextApiRoutes ||
|
|
37
|
-
detected.backendDirsPresent.length > 0;
|
|
52
|
+
const hasBackend = hasBackendEvidence(detected);
|
|
38
53
|
signals.push({
|
|
39
54
|
name: 'backend-presence',
|
|
40
55
|
matched: hasBackend,
|
|
@@ -44,8 +59,12 @@ function decideArchetype(detected) {
|
|
|
44
59
|
? `framework: ${detected.backendFrameworks.join(', ')}`
|
|
45
60
|
: null,
|
|
46
61
|
detected.hasNextApiRoutes ? 'next-api-routes' : null,
|
|
62
|
+
detected.hasNextServerActions ? 'next-server-actions' : null,
|
|
47
63
|
detected.backendDirsPresent.length > 0
|
|
48
64
|
? `dirs: ${detected.backendDirsPresent.join(', ')}`
|
|
65
|
+
: null,
|
|
66
|
+
detected.nestedServiceEvidence.length > 0
|
|
67
|
+
? `nested: ${detected.nestedServiceEvidence.join(', ')}`
|
|
49
68
|
: null
|
|
50
69
|
]
|
|
51
70
|
.filter(Boolean)
|
|
@@ -131,33 +150,24 @@ function decideFrontendOnly(report) {
|
|
|
131
150
|
if (report.archetype === 'legacy-frontend' || report.archetype === 'frontend-monorepo') {
|
|
132
151
|
return { frontendOnly: true, reason: `archetype=${report.archetype}` };
|
|
133
152
|
}
|
|
134
|
-
const noBackend = !report.detected
|
|
135
|
-
!report.detected.hasNextApiRoutes &&
|
|
136
|
-
report.detected.backendDirsPresent.length === 0;
|
|
153
|
+
const noBackend = !hasBackendEvidence(report.detected);
|
|
137
154
|
if (noBackend && !report.detected.hasSwaggerOrProto) {
|
|
138
155
|
return { frontendOnly: true, reason: 'no-backend-no-swagger' };
|
|
139
156
|
}
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
report.detected.backendDirsPresent.length > 0) {
|
|
143
|
-
return { frontendOnly: false, reason: 'backend-detected' };
|
|
157
|
+
if (noBackend) {
|
|
158
|
+
return { frontendOnly: false, reason: 'swagger-or-proto-present' };
|
|
144
159
|
}
|
|
145
|
-
return { frontendOnly: false, reason: '
|
|
160
|
+
return { frontendOnly: false, reason: 'backend-detected' };
|
|
146
161
|
}
|
|
147
162
|
/**
|
|
148
163
|
* Three frontend integration scenarios, from the signals `detected`
|
|
149
|
-
* already holds. Backend presence
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* project with `pages/api` is `legacy-fullstack` there and must not be
|
|
154
|
-
* `prd-only` here — one report cannot contradict itself.
|
|
164
|
+
* already holds. Backend presence is the SAME predicate `decideArchetype`
|
|
165
|
+
* uses (`hasBackendEvidence`), because one report cannot be
|
|
166
|
+
* `legacy-fullstack` and `prd-only` at once — a Next project with server
|
|
167
|
+
* actions, or an express service under `apps/`, is a backend for both.
|
|
155
168
|
*/
|
|
156
169
|
function decideIntegrationMode(report) {
|
|
157
|
-
|
|
158
|
-
report.detected.hasNextApiRoutes ||
|
|
159
|
-
report.detected.backendDirsPresent.length > 0;
|
|
160
|
-
if (hasBackend) {
|
|
170
|
+
if (hasBackendEvidence(report.detected)) {
|
|
161
171
|
return { integrationMode: 'full-stack', reason: 'backend-detected' };
|
|
162
172
|
}
|
|
163
173
|
if (report.detected.hasSwaggerOrProto) {
|
|
@@ -172,6 +182,8 @@ export async function scanArchetype(options) {
|
|
|
172
182
|
const hasNext = Object.prototype.hasOwnProperty.call(deps, 'next');
|
|
173
183
|
const hasNextApiRoutes = await detectNextApiRoutes(projectRoot, hasNext);
|
|
174
184
|
const backendDirsPresent = await detectBackendDirs(projectRoot);
|
|
185
|
+
const nestedServiceEvidence = await detectNestedServiceEvidence(projectRoot);
|
|
186
|
+
const hasNextServerActions = hasNext ? await detectNextServerActions(projectRoot) : false;
|
|
175
187
|
const swaggerPaths = await detectSwagger(projectRoot);
|
|
176
188
|
const monorepoConfigs = await detectMonorepoConfigs(projectRoot);
|
|
177
189
|
const srcFileCount = await countSrcFiles(projectRoot);
|
|
@@ -180,6 +192,8 @@ export async function scanArchetype(options) {
|
|
|
180
192
|
hasPackageJson,
|
|
181
193
|
hasBackendFramework: backendFrameworks.length > 0,
|
|
182
194
|
backendFrameworks,
|
|
195
|
+
nestedServiceEvidence,
|
|
196
|
+
hasNextServerActions,
|
|
183
197
|
hasSwaggerOrProto: swaggerPaths.length > 0,
|
|
184
198
|
swaggerPaths,
|
|
185
199
|
hasMonorepoConfig: monorepoConfigs.length > 0,
|
|
@@ -22,6 +22,15 @@ export type ArchetypeReport = {
|
|
|
22
22
|
hasPackageJson: boolean;
|
|
23
23
|
hasBackendFramework: boolean;
|
|
24
24
|
backendFrameworks: string[];
|
|
25
|
+
/** Backend evidence the ROOT manifest cannot see: a workspace package's own
|
|
26
|
+
* `package.json` (`apps/gateway: express`) and any non-Node service
|
|
27
|
+
* manifest (`go.mod`, `services/checkout/requirements.txt (fastapi)`).
|
|
28
|
+
* Every probe that filled `detected` before this field read Node's
|
|
29
|
+
* manifest only, so a Go or Java service reported as `frontendOnly`. */
|
|
30
|
+
nestedServiceEvidence: string[];
|
|
31
|
+
/** Next.js server actions (`'use server'` under `app/`). Route handlers
|
|
32
|
+
* (`pages/api` / `app/api`) were the only Next backend recognised. */
|
|
33
|
+
hasNextServerActions: boolean;
|
|
25
34
|
hasSwaggerOrProto: boolean;
|
|
26
35
|
swaggerPaths: string[];
|
|
27
36
|
hasMonorepoConfig: boolean;
|