@plainconceptsplatform/agent-harness 3.4.1 → 3.6.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/LICENSE +21 -21
- package/README.md +455 -454
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +120 -120
- package/cli/steps/copy/fullstack-engineer.js +89 -89
- package/cli/steps/copy/opencode-json.js +324 -324
- package/cli/steps/copy/skills.js +316 -316
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -141
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -91
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -134
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/paths.js +81 -81
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/_gitignore +10 -10
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -10
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -7
- package/harness/.opencode/commands/plan-explore.md +11 -11
- package/harness/.opencode/commands/plan-propose.md +11 -11
- package/harness/.opencode/commands/plan-quick.md +7 -7
- package/harness/.opencode/commands/plan-story.md +11 -11
- package/harness/.opencode/commands/repo-audit.md +7 -7
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/package.json +5 -5
- package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -0
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -367
- package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
- package/harness/.opencode/scripts/check-diff.mjs +252 -244
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +36 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +14 -12
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +88 -86
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-propose/SKILL.md +125 -125
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-verify/SKILL.md +150 -150
|
@@ -1,288 +1,288 @@
|
|
|
1
|
-
// The same rules Project-Health uses to score a repository (collector/src/metrics/code-rules.ts).
|
|
2
|
-
// Keep the two in step: a rule that differs makes the gate and the dashboard disagree.
|
|
3
|
-
|
|
4
|
-
const SOURCE = /\.(?:[cm]?[jt]sx?|cs)$/
|
|
5
|
-
const SKIP =
|
|
6
|
-
/(?:^|\/)(?:node_modules|dist|build|out|\.next|bin|obj|coverage|Migrations|migrations|wwwroot|vendor|generated|__generated__|\.github|\.opencode|\.agents|\.claude|openspec)(?:\/|$)|(?:^|\/)components\/ui\/|\.d\.ts$|\.g\.cs$|\.Designer\.cs$|\.min\.js$|AssemblyInfo\.cs$/
|
|
7
|
-
const TEST =
|
|
8
|
-
/(?:^|\/)(?:tests?|__tests__|e2e|specs?)\/|\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|\/)[\w.]+\.(?:Unit|Integration|E2E|Acceptance|Architecture)?Tests?\/|Tests?\.cs$/
|
|
9
|
-
const GENERATED_HEADER =
|
|
10
|
-
/Managed by @plainconceptsplatform|<auto-generated|@generated|This file was (?:auto-)?generated/i
|
|
11
|
-
|
|
12
|
-
export function kindOf(path) {
|
|
13
|
-
if (!SOURCE.test(path) || SKIP.test(path)) return null
|
|
14
|
-
return TEST.test(path) ? 'test' : 'source'
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function isGenerated(text) {
|
|
18
|
-
return GENERATED_HEADER.test(text.split('\n', 4).join('\n'))
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// A comment that is an instruction to a tool, not prose for a reader. Semgrep demands a
|
|
22
|
-
// `nosemgrep` justification on an empty catch; counting it against the comment budget made the
|
|
23
|
-
// two gates trade lines with each other.
|
|
24
|
-
const PRAGMA = /^(\/\/|\/\*)\s*(nosemgrep|biome-ignore|eslint-(disable|enable)|@ts-(expect-error|ignore)|prettier-ignore|(istanbul|c8|v8) ignore|stryker (disable|restore)|ReSharper (disable|restore)|NOSONAR)\b/i
|
|
25
|
-
|
|
26
|
-
export function lineKinds(text) {
|
|
27
|
-
const kinds = []
|
|
28
|
-
let inBlock = false
|
|
29
|
-
for (const raw of text.split('\n')) {
|
|
30
|
-
const t = raw.trim()
|
|
31
|
-
if (!t) kinds.push('blank')
|
|
32
|
-
else if (!inBlock && PRAGMA.test(t)) {
|
|
33
|
-
kinds.push('pragma')
|
|
34
|
-
inBlock = t.startsWith('/*') && !t.includes('*/')
|
|
35
|
-
} else if (inBlock) {
|
|
36
|
-
kinds.push('comment')
|
|
37
|
-
if (t.includes('*/')) inBlock = false
|
|
38
|
-
} else if (t.startsWith('/*')) {
|
|
39
|
-
kinds.push('comment')
|
|
40
|
-
inBlock = !t.includes('*/')
|
|
41
|
-
} else if (t.startsWith('//')) kinds.push('comment')
|
|
42
|
-
else kinds.push('code')
|
|
43
|
-
}
|
|
44
|
-
return kinds
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function countLines(text) {
|
|
48
|
-
let codeLines = 0
|
|
49
|
-
let commentLines = 0
|
|
50
|
-
for (const kind of lineKinds(text)) {
|
|
51
|
-
if (kind === 'code') codeLines += 1
|
|
52
|
-
else if (kind === 'comment') commentLines += 1
|
|
53
|
-
}
|
|
54
|
-
return { codeLines, commentLines }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const FILLER = new Set(
|
|
58
|
-
'a an the this that of for to in on is are be or and with by from its it get gets set sets getting setting value values instance class record struct interface enum new initializes initialises creates constructor property field method current specified given provided object function component hook type props'.split(' '),
|
|
59
|
-
)
|
|
60
|
-
const RETURNS_FILLER = new Set([
|
|
61
|
-
...FILLER,
|
|
62
|
-
'result', 'results', 'task', 'representing', 'asynchronous', 'async', 'operation', 'returns', 'returned', 'which', 'that',
|
|
63
|
-
])
|
|
64
|
-
|
|
65
|
-
const words = text =>
|
|
66
|
-
text
|
|
67
|
-
.replace(/<see\s+cref="([^"]+)"\s*\/>/g, ' $1 ')
|
|
68
|
-
.replace(/<paramref\s+name="([^"]+)"\s*\/>/g, ' $1 ')
|
|
69
|
-
.replace(/\{@link\s+([^}\s]+)[^}]*\}/g, ' $1 ')
|
|
70
|
-
.replace(/<[^>]+>/g, ' ')
|
|
71
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
72
|
-
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
|
|
73
|
-
.toLowerCase()
|
|
74
|
-
.split(/[^a-z0-9]+/)
|
|
75
|
-
.filter(Boolean)
|
|
76
|
-
|
|
77
|
-
const stem = w => (w.length > 3 && w.endsWith('s') && !w.endsWith('ss') ? w.slice(0, -1) : w)
|
|
78
|
-
|
|
79
|
-
export function restates(text, identifier, filler = FILLER) {
|
|
80
|
-
const name = new Set(words(identifier.replace(/^I(?=[A-Z])/, '')).map(stem))
|
|
81
|
-
const said = words(text).filter(w => !filler.has(w) && !filler.has(stem(w))).map(stem)
|
|
82
|
-
return said.every(w => name.has(w))
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const DECL_KEYWORD = /\b(?:class|record|struct|interface|enum|namespace|type|function)\s+([A-Za-z_$][\w$]*)/
|
|
86
|
-
const DECL_VAR = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/
|
|
87
|
-
const DECL_CALL = /([A-Za-z_$][\w$]*)\s*(?:<[^()]*>)?\s*\(/
|
|
88
|
-
const DECL_PROP = /([A-Za-z_$][\w$]*)\s*[?!]?\s*(?:\{|=>|=|;|:|$)/
|
|
89
|
-
|
|
90
|
-
export function declaredName(line) {
|
|
91
|
-
const l = line.trim()
|
|
92
|
-
if (!l || l.startsWith('[') || l.startsWith('@')) return null
|
|
93
|
-
return (l.match(DECL_KEYWORD) ?? l.match(DECL_VAR) ?? l.match(DECL_CALL) ?? l.match(DECL_PROP))?.[1] ?? null
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const BANNER = /^\/\/+\s*(?:([=\-*#~_+])\1{2,}.*|.*([=\-*#~_+])\2{2,})\s*$/
|
|
97
|
-
const REGION = /^#(?:region|endregion)\b/
|
|
98
|
-
const TODO = /\/\/.*\b(?:TODO|FIXME|HACK|XXX)\b/
|
|
99
|
-
const TRACKED = /#\d+|https?:\/\/|\b[A-Z][A-Z0-9]+-\d+\b|\(\s*issue\s/i
|
|
100
|
-
const APOLOGETIC =
|
|
101
|
-
/\b(?:not sure (?:if|whether|this|why)|hope this works|might not work|probably wrong|no idea why|i think this (?:works|is right)|quick and dirty)\b/i
|
|
102
|
-
const CODE_BODY =
|
|
103
|
-
/^(?:(?:var|let|const|await|return|using|import|export|if|else|for|foreach|while|switch|throw|new|public|private|protected|internal|static|async)\b.*[;{(]\s*$|[\w.$[\]]+\s*(?:\+\+|--|[+\-*/]?=)\s*[^=].*;\s*$|[\w.$]+(?:<[^>]*>)?\([^)]*\)\s*;\s*$|[{}]\s*;?\s*$|\}\s*else\b.*$)/
|
|
104
|
-
const TOOL_PRAGMA =
|
|
105
|
-
/^(?:nosemgrep|eslint|biome|jscpd|@ts-|prettier|istanbul|c8|v8|webpack|vite|ReSharper|resharper|language=|<auto-generated|#pragma)/
|
|
106
|
-
|
|
107
|
-
const bodyOf = trimmed => trimmed.replace(/^\/\/+\s?/, '')
|
|
108
|
-
|
|
109
|
-
export function trailingComment(line) {
|
|
110
|
-
let quote = null
|
|
111
|
-
for (let i = 0; i < line.length; i++) {
|
|
112
|
-
const c = line[i]
|
|
113
|
-
if (quote) {
|
|
114
|
-
if (c === '\\') i += 1
|
|
115
|
-
else if (c === quote) quote = null
|
|
116
|
-
} else if (c === "'" || c === '"' || c === '`') quote = c
|
|
117
|
-
else if (c === '/' && line[i + 1] === '/' && line[i - 1] !== ':') return line.slice(i)
|
|
118
|
-
}
|
|
119
|
-
return null
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function slopFindings(text) {
|
|
123
|
-
const lines = text.split('\n')
|
|
124
|
-
const out = []
|
|
125
|
-
const add = (rule, line) => out.push({ rule, line })
|
|
126
|
-
const nameAfter = from => {
|
|
127
|
-
for (let k = from; k < lines.length && k < from + 6; k++) {
|
|
128
|
-
const name = declaredName(lines[k])
|
|
129
|
-
if (name) return name
|
|
130
|
-
}
|
|
131
|
-
return null
|
|
132
|
-
}
|
|
133
|
-
let codeRun = false
|
|
134
|
-
for (let i = 0; i < lines.length; i++) {
|
|
135
|
-
const t = lines[i].trim()
|
|
136
|
-
const isLineComment = t.startsWith('//') && !t.startsWith('///')
|
|
137
|
-
const body = isLineComment ? bodyOf(t).trim() : ''
|
|
138
|
-
const isCode =
|
|
139
|
-
isLineComment && body !== '' && !TOOL_PRAGMA.test(body) && CODE_BODY.test(body) && !/\s(?:https?:)?\/\//.test(body)
|
|
140
|
-
if (isCode && !codeRun) add('commented-out-code', i + 1)
|
|
141
|
-
codeRun = isCode || (codeRun && isLineComment && !body)
|
|
142
|
-
|
|
143
|
-
if (isLineComment && BANNER.test(t)) add('banner', i + 1)
|
|
144
|
-
if (REGION.test(t)) add('region', i + 1)
|
|
145
|
-
const comment = trailingComment(t)
|
|
146
|
-
if (comment && TODO.test(comment) && !TRACKED.test(comment)) add('untracked-todo', i + 1)
|
|
147
|
-
if ((t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) && APOLOGETIC.test(t)) add('apologetic', i + 1)
|
|
148
|
-
|
|
149
|
-
if (t.startsWith('///') && t.includes('<summary>')) {
|
|
150
|
-
const start = i
|
|
151
|
-
let doc = ''
|
|
152
|
-
let j = i
|
|
153
|
-
for (; j < lines.length && lines[j].trim().startsWith('///'); j++) {
|
|
154
|
-
doc += ` ${lines[j].trim().replace(/^\/\/\/\s?/, '')}`
|
|
155
|
-
}
|
|
156
|
-
const name = nameAfter(j)
|
|
157
|
-
const summary = doc.match(/<summary>([\s\S]*?)<\/summary>/)?.[1] ?? ''
|
|
158
|
-
if (name && summary.trim() && restates(summary, name)) add('restating-summary', start + 1)
|
|
159
|
-
for (const m of doc.matchAll(/<param\s+name="([^"]+)"\s*>([\s\S]*?)<\/param>/g)) {
|
|
160
|
-
if (m[2].trim() && restates(m[2], m[1])) add('restating-param', start + 1)
|
|
161
|
-
}
|
|
162
|
-
for (const m of doc.matchAll(/<returns>([\s\S]*?)<\/returns>/g)) {
|
|
163
|
-
if (m[1].trim() && restates(m[1], '', RETURNS_FILLER)) add('filler-returns', start + 1)
|
|
164
|
-
}
|
|
165
|
-
i = j - 1
|
|
166
|
-
} else if (t.startsWith('/**') && !t.startsWith('/***')) {
|
|
167
|
-
const start = i
|
|
168
|
-
let j = i
|
|
169
|
-
let doc = ''
|
|
170
|
-
for (; j < lines.length; j++) {
|
|
171
|
-
doc += ` ${lines[j].trim()}`
|
|
172
|
-
if (lines[j].includes('*/')) break
|
|
173
|
-
}
|
|
174
|
-
const text = doc.replace(/^\s*\/\*\*|\*\/\s*$/g, ' ').replace(/\s\*\s?/g, ' ').trim()
|
|
175
|
-
const summary = text.split(/\s@\w+/)[0]?.trim() ?? ''
|
|
176
|
-
const name = nameAfter(j + 1)
|
|
177
|
-
if (name && summary && restates(summary, name)) add('restating-jsdoc', start + 1)
|
|
178
|
-
for (const m of text.matchAll(/@param\s+(?:\{[^}]*\}\s*)?\[?([\w$.]+)\]?\s*(?:-\s*)?([^@]*)/g)) {
|
|
179
|
-
if (m[2].trim() && restates(m[2], m[1])) add('restating-param', start + 1)
|
|
180
|
-
}
|
|
181
|
-
for (const m of text.matchAll(/@returns?\s+(?:\{[^}]*\}\s*)?([^@]*)/g)) {
|
|
182
|
-
if (m[1].trim() && restates(m[1], '', RETURNS_FILLER)) add('filler-returns', start + 1)
|
|
183
|
-
}
|
|
184
|
-
i = j
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return out
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const ISSUE_REF = /(?:\b[A-Z][\w-]*\s)?#\d+\b|\b[A-Z][A-Z0-9]+-\d+\b/
|
|
191
|
-
const PAST =
|
|
192
|
-
/\b(?:was|were|used to|previously|before|had|broke|regressed|ended up|turned out|once|anymore|no longer|caused)\b/i
|
|
193
|
-
const TS_INTERNAL = /^(?:async\s+)?(?:function|const|let|var|class|interface|type|enum)\b/
|
|
194
|
-
const CS_INTERNAL = /^(?:private|internal|private\s+protected)\b/
|
|
195
|
-
|
|
196
|
-
export function commentBlocks(text) {
|
|
197
|
-
const lines = text.split('\n')
|
|
198
|
-
const blocks = []
|
|
199
|
-
const firstCode = lines.findIndex(l => {
|
|
200
|
-
const t = l.trim()
|
|
201
|
-
return t !== '' && !t.startsWith('//') && !t.startsWith('/*') && !t.startsWith('*')
|
|
202
|
-
})
|
|
203
|
-
let i = 0
|
|
204
|
-
while (i < lines.length) {
|
|
205
|
-
const t = lines[i].trim()
|
|
206
|
-
if (!(t.startsWith('//') || t.startsWith('/*'))) {
|
|
207
|
-
i += 1
|
|
208
|
-
continue
|
|
209
|
-
}
|
|
210
|
-
const start = i
|
|
211
|
-
let inBlock = false
|
|
212
|
-
let body = ''
|
|
213
|
-
for (; i < lines.length; i++) {
|
|
214
|
-
const s = lines[i].trim()
|
|
215
|
-
if (inBlock) {
|
|
216
|
-
body += ` ${s}`
|
|
217
|
-
if (s.includes('*/')) inBlock = false
|
|
218
|
-
continue
|
|
219
|
-
}
|
|
220
|
-
if (s.startsWith('/*')) {
|
|
221
|
-
body += ` ${s}`
|
|
222
|
-
inBlock = !s.includes('*/')
|
|
223
|
-
continue
|
|
224
|
-
}
|
|
225
|
-
if (s.startsWith('//')) {
|
|
226
|
-
body += ` ${s}`
|
|
227
|
-
continue
|
|
228
|
-
}
|
|
229
|
-
break
|
|
230
|
-
}
|
|
231
|
-
const rules = []
|
|
232
|
-
if (ISSUE_REF.test(body) && PAST.test(body)) rules.push('history')
|
|
233
|
-
if (i - start >= 5 && start > firstCode) rules.push('essay')
|
|
234
|
-
if (body.trimStart().startsWith('/**') || body.trimStart().startsWith('///')) {
|
|
235
|
-
const next = lines.slice(i, i + 4).find(l => {
|
|
236
|
-
const s = l.trim()
|
|
237
|
-
return s !== '' && !s.startsWith('[') && !s.startsWith('@')
|
|
238
|
-
})
|
|
239
|
-
const decl = next?.trim() ?? ''
|
|
240
|
-
if (TS_INTERNAL.test(decl) || CS_INTERNAL.test(decl)) rules.push('internal-doc')
|
|
241
|
-
}
|
|
242
|
-
blocks.push({ start: start + 1, end: i, rules })
|
|
243
|
-
}
|
|
244
|
-
return blocks
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const TS_CASE =
|
|
248
|
-
/(?:^|[^\w.$])(x|f)?(?:it|test)(\.only|\.skip|\.todo|\.concurrent|\.each\s*(?:\([^)]*\)|`[^`]*`))?\s*\(\s*['"`]/g
|
|
249
|
-
const CS_CASE = /\[(?:Fact|Theory|SkippableFact|SkippableTheory|Test|TestMethod)\b([^\]]*)\]/g
|
|
250
|
-
const TS_ASSERT = /\b(?:expect|assert)\w*(?:\.\w+)?\s*\(|\.should\b/g
|
|
251
|
-
const TS_CALL_ASSERT = /\.(?:not\.)?(?:toHaveBeenCalled\w*|toBeCalled\w*|toHaveBeenNthCalledWith)\s*\(/g
|
|
252
|
-
const CS_ASSERT = /\bAssert\w*\.\w+|\.Should\w*\(|\bVerify\w*\(|\bThrows\w*(?:Async)?\s*[<(]|\.Received\(/g
|
|
253
|
-
const CS_CALL_ASSERT = /\.Received\(|\.DidNotReceive\(|\.Verify\(\s*\w+\s*=>/g
|
|
254
|
-
const SNAPSHOT = /\.toMatch(?:Inline|File)?Snapshot\s*\(|\bawait\s+Verify(?:Json|Xml)?\s*\(/
|
|
255
|
-
|
|
256
|
-
const count = (text, re) => (text.match(re) ?? []).length
|
|
257
|
-
|
|
258
|
-
export function testStats(path, text) {
|
|
259
|
-
const cs = path.endsWith('.cs')
|
|
260
|
-
const starts = []
|
|
261
|
-
for (const m of text.matchAll(cs ? CS_CASE : TS_CASE)) {
|
|
262
|
-
if (cs) {
|
|
263
|
-
starts.push({ index: m.index, skipped: /\bSkip\s*=/.test(m[1] ?? ''), focused: false })
|
|
264
|
-
} else {
|
|
265
|
-
const modifier = m[2] ?? ''
|
|
266
|
-
if (modifier === '.todo') continue
|
|
267
|
-
starts.push({ index: m.index, skipped: m[1] === 'x' || modifier === '.skip', focused: m[1] === 'f' || modifier === '.only' })
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
const stats = { cases: starts.length, skipped: 0, focused: 0, snapshot: 0, noAssertion: 0, mockOnly: 0 }
|
|
271
|
-
if (cs) stats.skipped += count(text, /\[Ignore\b/g)
|
|
272
|
-
else {
|
|
273
|
-
stats.skipped += count(text, /\b(?:describe\.skip|xdescribe)\s*\(/g)
|
|
274
|
-
stats.focused += count(text, /\b(?:describe\.only|fdescribe)\s*\(/g)
|
|
275
|
-
}
|
|
276
|
-
for (let k = 0; k < starts.length; k++) {
|
|
277
|
-
const s = starts[k]
|
|
278
|
-
const segment = text.slice(s.index, starts[k + 1]?.index ?? text.length)
|
|
279
|
-
if (s.skipped) stats.skipped += 1
|
|
280
|
-
if (s.focused) stats.focused += 1
|
|
281
|
-
if (SNAPSHOT.test(segment)) stats.snapshot += 1
|
|
282
|
-
const asserts = count(segment, cs ? CS_ASSERT : TS_ASSERT)
|
|
283
|
-
const callAsserts = count(segment, cs ? CS_CALL_ASSERT : TS_CALL_ASSERT)
|
|
284
|
-
if (asserts === 0) stats.noAssertion += 1
|
|
285
|
-
else if (callAsserts > 0 && callAsserts >= asserts) stats.mockOnly += 1
|
|
286
|
-
}
|
|
287
|
-
return stats
|
|
288
|
-
}
|
|
1
|
+
// The same rules Project-Health uses to score a repository (collector/src/metrics/code-rules.ts).
|
|
2
|
+
// Keep the two in step: a rule that differs makes the gate and the dashboard disagree.
|
|
3
|
+
|
|
4
|
+
const SOURCE = /\.(?:[cm]?[jt]sx?|cs)$/
|
|
5
|
+
const SKIP =
|
|
6
|
+
/(?:^|\/)(?:node_modules|dist|build|out|\.next|bin|obj|coverage|Migrations|migrations|wwwroot|vendor|generated|__generated__|\.github|\.opencode|\.agents|\.claude|openspec)(?:\/|$)|(?:^|\/)components\/ui\/|\.d\.ts$|\.g\.cs$|\.Designer\.cs$|\.min\.js$|AssemblyInfo\.cs$/
|
|
7
|
+
const TEST =
|
|
8
|
+
/(?:^|\/)(?:tests?|__tests__|e2e|specs?)\/|\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|\/)[\w.]+\.(?:Unit|Integration|E2E|Acceptance|Architecture)?Tests?\/|Tests?\.cs$/
|
|
9
|
+
const GENERATED_HEADER =
|
|
10
|
+
/Managed by @plainconceptsplatform|<auto-generated|@generated|This file was (?:auto-)?generated/i
|
|
11
|
+
|
|
12
|
+
export function kindOf(path) {
|
|
13
|
+
if (!SOURCE.test(path) || SKIP.test(path)) return null
|
|
14
|
+
return TEST.test(path) ? 'test' : 'source'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isGenerated(text) {
|
|
18
|
+
return GENERATED_HEADER.test(text.split('\n', 4).join('\n'))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A comment that is an instruction to a tool, not prose for a reader. Semgrep demands a
|
|
22
|
+
// `nosemgrep` justification on an empty catch; counting it against the comment budget made the
|
|
23
|
+
// two gates trade lines with each other.
|
|
24
|
+
const PRAGMA = /^(\/\/|\/\*)\s*(nosemgrep|biome-ignore|eslint-(disable|enable)|@ts-(expect-error|ignore)|prettier-ignore|(istanbul|c8|v8) ignore|stryker (disable|restore)|ReSharper (disable|restore)|NOSONAR)\b/i
|
|
25
|
+
|
|
26
|
+
export function lineKinds(text) {
|
|
27
|
+
const kinds = []
|
|
28
|
+
let inBlock = false
|
|
29
|
+
for (const raw of text.split('\n')) {
|
|
30
|
+
const t = raw.trim()
|
|
31
|
+
if (!t) kinds.push('blank')
|
|
32
|
+
else if (!inBlock && PRAGMA.test(t)) {
|
|
33
|
+
kinds.push('pragma')
|
|
34
|
+
inBlock = t.startsWith('/*') && !t.includes('*/')
|
|
35
|
+
} else if (inBlock) {
|
|
36
|
+
kinds.push('comment')
|
|
37
|
+
if (t.includes('*/')) inBlock = false
|
|
38
|
+
} else if (t.startsWith('/*')) {
|
|
39
|
+
kinds.push('comment')
|
|
40
|
+
inBlock = !t.includes('*/')
|
|
41
|
+
} else if (t.startsWith('//')) kinds.push('comment')
|
|
42
|
+
else kinds.push('code')
|
|
43
|
+
}
|
|
44
|
+
return kinds
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function countLines(text) {
|
|
48
|
+
let codeLines = 0
|
|
49
|
+
let commentLines = 0
|
|
50
|
+
for (const kind of lineKinds(text)) {
|
|
51
|
+
if (kind === 'code') codeLines += 1
|
|
52
|
+
else if (kind === 'comment') commentLines += 1
|
|
53
|
+
}
|
|
54
|
+
return { codeLines, commentLines }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const FILLER = new Set(
|
|
58
|
+
'a an the this that of for to in on is are be or and with by from its it get gets set sets getting setting value values instance class record struct interface enum new initializes initialises creates constructor property field method current specified given provided object function component hook type props'.split(' '),
|
|
59
|
+
)
|
|
60
|
+
const RETURNS_FILLER = new Set([
|
|
61
|
+
...FILLER,
|
|
62
|
+
'result', 'results', 'task', 'representing', 'asynchronous', 'async', 'operation', 'returns', 'returned', 'which', 'that',
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
const words = text =>
|
|
66
|
+
text
|
|
67
|
+
.replace(/<see\s+cref="([^"]+)"\s*\/>/g, ' $1 ')
|
|
68
|
+
.replace(/<paramref\s+name="([^"]+)"\s*\/>/g, ' $1 ')
|
|
69
|
+
.replace(/\{@link\s+([^}\s]+)[^}]*\}/g, ' $1 ')
|
|
70
|
+
.replace(/<[^>]+>/g, ' ')
|
|
71
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
72
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.split(/[^a-z0-9]+/)
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
|
|
77
|
+
const stem = w => (w.length > 3 && w.endsWith('s') && !w.endsWith('ss') ? w.slice(0, -1) : w)
|
|
78
|
+
|
|
79
|
+
export function restates(text, identifier, filler = FILLER) {
|
|
80
|
+
const name = new Set(words(identifier.replace(/^I(?=[A-Z])/, '')).map(stem))
|
|
81
|
+
const said = words(text).filter(w => !filler.has(w) && !filler.has(stem(w))).map(stem)
|
|
82
|
+
return said.every(w => name.has(w))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const DECL_KEYWORD = /\b(?:class|record|struct|interface|enum|namespace|type|function)\s+([A-Za-z_$][\w$]*)/
|
|
86
|
+
const DECL_VAR = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/
|
|
87
|
+
const DECL_CALL = /([A-Za-z_$][\w$]*)\s*(?:<[^()]*>)?\s*\(/
|
|
88
|
+
const DECL_PROP = /([A-Za-z_$][\w$]*)\s*[?!]?\s*(?:\{|=>|=|;|:|$)/
|
|
89
|
+
|
|
90
|
+
export function declaredName(line) {
|
|
91
|
+
const l = line.trim()
|
|
92
|
+
if (!l || l.startsWith('[') || l.startsWith('@')) return null
|
|
93
|
+
return (l.match(DECL_KEYWORD) ?? l.match(DECL_VAR) ?? l.match(DECL_CALL) ?? l.match(DECL_PROP))?.[1] ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const BANNER = /^\/\/+\s*(?:([=\-*#~_+])\1{2,}.*|.*([=\-*#~_+])\2{2,})\s*$/
|
|
97
|
+
const REGION = /^#(?:region|endregion)\b/
|
|
98
|
+
const TODO = /\/\/.*\b(?:TODO|FIXME|HACK|XXX)\b/
|
|
99
|
+
const TRACKED = /#\d+|https?:\/\/|\b[A-Z][A-Z0-9]+-\d+\b|\(\s*issue\s/i
|
|
100
|
+
const APOLOGETIC =
|
|
101
|
+
/\b(?:not sure (?:if|whether|this|why)|hope this works|might not work|probably wrong|no idea why|i think this (?:works|is right)|quick and dirty)\b/i
|
|
102
|
+
const CODE_BODY =
|
|
103
|
+
/^(?:(?:var|let|const|await|return|using|import|export|if|else|for|foreach|while|switch|throw|new|public|private|protected|internal|static|async)\b.*[;{(]\s*$|[\w.$[\]]+\s*(?:\+\+|--|[+\-*/]?=)\s*[^=].*;\s*$|[\w.$]+(?:<[^>]*>)?\([^)]*\)\s*;\s*$|[{}]\s*;?\s*$|\}\s*else\b.*$)/
|
|
104
|
+
const TOOL_PRAGMA =
|
|
105
|
+
/^(?:nosemgrep|eslint|biome|jscpd|@ts-|prettier|istanbul|c8|v8|webpack|vite|ReSharper|resharper|language=|<auto-generated|#pragma)/
|
|
106
|
+
|
|
107
|
+
const bodyOf = trimmed => trimmed.replace(/^\/\/+\s?/, '')
|
|
108
|
+
|
|
109
|
+
export function trailingComment(line) {
|
|
110
|
+
let quote = null
|
|
111
|
+
for (let i = 0; i < line.length; i++) {
|
|
112
|
+
const c = line[i]
|
|
113
|
+
if (quote) {
|
|
114
|
+
if (c === '\\') i += 1
|
|
115
|
+
else if (c === quote) quote = null
|
|
116
|
+
} else if (c === "'" || c === '"' || c === '`') quote = c
|
|
117
|
+
else if (c === '/' && line[i + 1] === '/' && line[i - 1] !== ':') return line.slice(i)
|
|
118
|
+
}
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function slopFindings(text) {
|
|
123
|
+
const lines = text.split('\n')
|
|
124
|
+
const out = []
|
|
125
|
+
const add = (rule, line) => out.push({ rule, line })
|
|
126
|
+
const nameAfter = from => {
|
|
127
|
+
for (let k = from; k < lines.length && k < from + 6; k++) {
|
|
128
|
+
const name = declaredName(lines[k])
|
|
129
|
+
if (name) return name
|
|
130
|
+
}
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
let codeRun = false
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
const t = lines[i].trim()
|
|
136
|
+
const isLineComment = t.startsWith('//') && !t.startsWith('///')
|
|
137
|
+
const body = isLineComment ? bodyOf(t).trim() : ''
|
|
138
|
+
const isCode =
|
|
139
|
+
isLineComment && body !== '' && !TOOL_PRAGMA.test(body) && CODE_BODY.test(body) && !/\s(?:https?:)?\/\//.test(body)
|
|
140
|
+
if (isCode && !codeRun) add('commented-out-code', i + 1)
|
|
141
|
+
codeRun = isCode || (codeRun && isLineComment && !body)
|
|
142
|
+
|
|
143
|
+
if (isLineComment && BANNER.test(t)) add('banner', i + 1)
|
|
144
|
+
if (REGION.test(t)) add('region', i + 1)
|
|
145
|
+
const comment = trailingComment(t)
|
|
146
|
+
if (comment && TODO.test(comment) && !TRACKED.test(comment)) add('untracked-todo', i + 1)
|
|
147
|
+
if ((t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) && APOLOGETIC.test(t)) add('apologetic', i + 1)
|
|
148
|
+
|
|
149
|
+
if (t.startsWith('///') && t.includes('<summary>')) {
|
|
150
|
+
const start = i
|
|
151
|
+
let doc = ''
|
|
152
|
+
let j = i
|
|
153
|
+
for (; j < lines.length && lines[j].trim().startsWith('///'); j++) {
|
|
154
|
+
doc += ` ${lines[j].trim().replace(/^\/\/\/\s?/, '')}`
|
|
155
|
+
}
|
|
156
|
+
const name = nameAfter(j)
|
|
157
|
+
const summary = doc.match(/<summary>([\s\S]*?)<\/summary>/)?.[1] ?? ''
|
|
158
|
+
if (name && summary.trim() && restates(summary, name)) add('restating-summary', start + 1)
|
|
159
|
+
for (const m of doc.matchAll(/<param\s+name="([^"]+)"\s*>([\s\S]*?)<\/param>/g)) {
|
|
160
|
+
if (m[2].trim() && restates(m[2], m[1])) add('restating-param', start + 1)
|
|
161
|
+
}
|
|
162
|
+
for (const m of doc.matchAll(/<returns>([\s\S]*?)<\/returns>/g)) {
|
|
163
|
+
if (m[1].trim() && restates(m[1], '', RETURNS_FILLER)) add('filler-returns', start + 1)
|
|
164
|
+
}
|
|
165
|
+
i = j - 1
|
|
166
|
+
} else if (t.startsWith('/**') && !t.startsWith('/***')) {
|
|
167
|
+
const start = i
|
|
168
|
+
let j = i
|
|
169
|
+
let doc = ''
|
|
170
|
+
for (; j < lines.length; j++) {
|
|
171
|
+
doc += ` ${lines[j].trim()}`
|
|
172
|
+
if (lines[j].includes('*/')) break
|
|
173
|
+
}
|
|
174
|
+
const text = doc.replace(/^\s*\/\*\*|\*\/\s*$/g, ' ').replace(/\s\*\s?/g, ' ').trim()
|
|
175
|
+
const summary = text.split(/\s@\w+/)[0]?.trim() ?? ''
|
|
176
|
+
const name = nameAfter(j + 1)
|
|
177
|
+
if (name && summary && restates(summary, name)) add('restating-jsdoc', start + 1)
|
|
178
|
+
for (const m of text.matchAll(/@param\s+(?:\{[^}]*\}\s*)?\[?([\w$.]+)\]?\s*(?:-\s*)?([^@]*)/g)) {
|
|
179
|
+
if (m[2].trim() && restates(m[2], m[1])) add('restating-param', start + 1)
|
|
180
|
+
}
|
|
181
|
+
for (const m of text.matchAll(/@returns?\s+(?:\{[^}]*\}\s*)?([^@]*)/g)) {
|
|
182
|
+
if (m[1].trim() && restates(m[1], '', RETURNS_FILLER)) add('filler-returns', start + 1)
|
|
183
|
+
}
|
|
184
|
+
i = j
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const ISSUE_REF = /(?:\b[A-Z][\w-]*\s)?#\d+\b|\b[A-Z][A-Z0-9]+-\d+\b/
|
|
191
|
+
const PAST =
|
|
192
|
+
/\b(?:was|were|used to|previously|before|had|broke|regressed|ended up|turned out|once|anymore|no longer|caused)\b/i
|
|
193
|
+
const TS_INTERNAL = /^(?:async\s+)?(?:function|const|let|var|class|interface|type|enum)\b/
|
|
194
|
+
const CS_INTERNAL = /^(?:private|internal|private\s+protected)\b/
|
|
195
|
+
|
|
196
|
+
export function commentBlocks(text) {
|
|
197
|
+
const lines = text.split('\n')
|
|
198
|
+
const blocks = []
|
|
199
|
+
const firstCode = lines.findIndex(l => {
|
|
200
|
+
const t = l.trim()
|
|
201
|
+
return t !== '' && !t.startsWith('//') && !t.startsWith('/*') && !t.startsWith('*')
|
|
202
|
+
})
|
|
203
|
+
let i = 0
|
|
204
|
+
while (i < lines.length) {
|
|
205
|
+
const t = lines[i].trim()
|
|
206
|
+
if (!(t.startsWith('//') || t.startsWith('/*'))) {
|
|
207
|
+
i += 1
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
const start = i
|
|
211
|
+
let inBlock = false
|
|
212
|
+
let body = ''
|
|
213
|
+
for (; i < lines.length; i++) {
|
|
214
|
+
const s = lines[i].trim()
|
|
215
|
+
if (inBlock) {
|
|
216
|
+
body += ` ${s}`
|
|
217
|
+
if (s.includes('*/')) inBlock = false
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
if (s.startsWith('/*')) {
|
|
221
|
+
body += ` ${s}`
|
|
222
|
+
inBlock = !s.includes('*/')
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (s.startsWith('//')) {
|
|
226
|
+
body += ` ${s}`
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
const rules = []
|
|
232
|
+
if (ISSUE_REF.test(body) && PAST.test(body)) rules.push('history')
|
|
233
|
+
if (i - start >= 5 && start > firstCode) rules.push('essay')
|
|
234
|
+
if (body.trimStart().startsWith('/**') || body.trimStart().startsWith('///')) {
|
|
235
|
+
const next = lines.slice(i, i + 4).find(l => {
|
|
236
|
+
const s = l.trim()
|
|
237
|
+
return s !== '' && !s.startsWith('[') && !s.startsWith('@')
|
|
238
|
+
})
|
|
239
|
+
const decl = next?.trim() ?? ''
|
|
240
|
+
if (TS_INTERNAL.test(decl) || CS_INTERNAL.test(decl)) rules.push('internal-doc')
|
|
241
|
+
}
|
|
242
|
+
blocks.push({ start: start + 1, end: i, rules })
|
|
243
|
+
}
|
|
244
|
+
return blocks
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const TS_CASE =
|
|
248
|
+
/(?:^|[^\w.$])(x|f)?(?:it|test)(\.only|\.skip|\.todo|\.concurrent|\.each\s*(?:\([^)]*\)|`[^`]*`))?\s*\(\s*['"`]/g
|
|
249
|
+
const CS_CASE = /\[(?:Fact|Theory|SkippableFact|SkippableTheory|Test|TestMethod)\b([^\]]*)\]/g
|
|
250
|
+
const TS_ASSERT = /\b(?:expect|assert)\w*(?:\.\w+)?\s*\(|\.should\b/g
|
|
251
|
+
const TS_CALL_ASSERT = /\.(?:not\.)?(?:toHaveBeenCalled\w*|toBeCalled\w*|toHaveBeenNthCalledWith)\s*\(/g
|
|
252
|
+
const CS_ASSERT = /\bAssert\w*\.\w+|\.Should\w*\(|\bVerify\w*\(|\bThrows\w*(?:Async)?\s*[<(]|\.Received\(/g
|
|
253
|
+
const CS_CALL_ASSERT = /\.Received\(|\.DidNotReceive\(|\.Verify\(\s*\w+\s*=>/g
|
|
254
|
+
const SNAPSHOT = /\.toMatch(?:Inline|File)?Snapshot\s*\(|\bawait\s+Verify(?:Json|Xml)?\s*\(/
|
|
255
|
+
|
|
256
|
+
const count = (text, re) => (text.match(re) ?? []).length
|
|
257
|
+
|
|
258
|
+
export function testStats(path, text) {
|
|
259
|
+
const cs = path.endsWith('.cs')
|
|
260
|
+
const starts = []
|
|
261
|
+
for (const m of text.matchAll(cs ? CS_CASE : TS_CASE)) {
|
|
262
|
+
if (cs) {
|
|
263
|
+
starts.push({ index: m.index, skipped: /\bSkip\s*=/.test(m[1] ?? ''), focused: false })
|
|
264
|
+
} else {
|
|
265
|
+
const modifier = m[2] ?? ''
|
|
266
|
+
if (modifier === '.todo') continue
|
|
267
|
+
starts.push({ index: m.index, skipped: m[1] === 'x' || modifier === '.skip', focused: m[1] === 'f' || modifier === '.only' })
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const stats = { cases: starts.length, skipped: 0, focused: 0, snapshot: 0, noAssertion: 0, mockOnly: 0 }
|
|
271
|
+
if (cs) stats.skipped += count(text, /\[Ignore\b/g)
|
|
272
|
+
else {
|
|
273
|
+
stats.skipped += count(text, /\b(?:describe\.skip|xdescribe)\s*\(/g)
|
|
274
|
+
stats.focused += count(text, /\b(?:describe\.only|fdescribe)\s*\(/g)
|
|
275
|
+
}
|
|
276
|
+
for (let k = 0; k < starts.length; k++) {
|
|
277
|
+
const s = starts[k]
|
|
278
|
+
const segment = text.slice(s.index, starts[k + 1]?.index ?? text.length)
|
|
279
|
+
if (s.skipped) stats.skipped += 1
|
|
280
|
+
if (s.focused) stats.focused += 1
|
|
281
|
+
if (SNAPSHOT.test(segment)) stats.snapshot += 1
|
|
282
|
+
const asserts = count(segment, cs ? CS_ASSERT : TS_ASSERT)
|
|
283
|
+
const callAsserts = count(segment, cs ? CS_CALL_ASSERT : TS_CALL_ASSERT)
|
|
284
|
+
if (asserts === 0) stats.noAssertion += 1
|
|
285
|
+
else if (callAsserts > 0 && callAsserts >= asserts) stats.mockOnly += 1
|
|
286
|
+
}
|
|
287
|
+
return stats
|
|
288
|
+
}
|
package/harness/ARCHITECTURE.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
> NOT GENERATED YET
|
|
2
|
-
>
|
|
3
|
-
> This file has not been populated yet. It is intentionally empty.
|
|
4
|
-
>
|
|
5
|
-
> **If this is a greenfield project** (no codebase exists yet): skip this for now. Come back and run `/make-architecture` once you have meaningful code, structure, or infrastructure in place.
|
|
6
|
-
>
|
|
7
|
-
> **If this is a brownfield project** (existing codebase): run this command now to generate the architecture documentation:
|
|
8
|
-
>
|
|
9
|
-
> ```
|
|
10
|
-
> /make-architecture
|
|
11
|
-
> ```
|
|
12
|
-
>
|
|
13
|
-
> This command analyzes your folder structure, config files, routes, data models, integrations, and build setup, then writes a complete ARCHITECTURE.md covering components, data flow, tech stack, deployment, and more. It is safe to rerun any time the architecture changes significantly.
|
|
1
|
+
> NOT GENERATED YET
|
|
2
|
+
>
|
|
3
|
+
> This file has not been populated yet. It is intentionally empty.
|
|
4
|
+
>
|
|
5
|
+
> **If this is a greenfield project** (no codebase exists yet): skip this for now. Come back and run `/make-architecture` once you have meaningful code, structure, or infrastructure in place.
|
|
6
|
+
>
|
|
7
|
+
> **If this is a brownfield project** (existing codebase): run this command now to generate the architecture documentation:
|
|
8
|
+
>
|
|
9
|
+
> ```
|
|
10
|
+
> /make-architecture
|
|
11
|
+
> ```
|
|
12
|
+
>
|
|
13
|
+
> This command analyzes your folder structure, config files, routes, data models, integrations, and build setup, then writes a complete ARCHITECTURE.md covering components, data flow, tech stack, deployment, and more. It is safe to rerun any time the architecture changes significantly.
|
package/harness/DESIGN.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
> NOT GENERATED YET
|
|
2
|
-
>
|
|
3
|
-
> This file has not been populated yet. It is intentionally empty.
|
|
4
|
-
>
|
|
5
|
-
> **If this is a greenfield project** (no UI exists yet): skip this for now. Come back and run `/make-design` once you have a design system, UI components, or styles in place.
|
|
6
|
-
>
|
|
7
|
-
> **If this is a brownfield project** (existing UI/styles): run this command now to generate the design documentation:
|
|
8
|
-
>
|
|
9
|
-
> ```
|
|
10
|
-
> /make-design
|
|
11
|
-
> ```
|
|
12
|
-
>
|
|
13
|
-
> This command analyzes your CSS, Tailwind config, component files, and design tokens, then writes a complete DESIGN.md with structured YAML tokens and written design intent. It is safe to rerun any time your design system changes.
|
|
1
|
+
> NOT GENERATED YET
|
|
2
|
+
>
|
|
3
|
+
> This file has not been populated yet. It is intentionally empty.
|
|
4
|
+
>
|
|
5
|
+
> **If this is a greenfield project** (no UI exists yet): skip this for now. Come back and run `/make-design` once you have a design system, UI components, or styles in place.
|
|
6
|
+
>
|
|
7
|
+
> **If this is a brownfield project** (existing UI/styles): run this command now to generate the design documentation:
|
|
8
|
+
>
|
|
9
|
+
> ```
|
|
10
|
+
> /make-design
|
|
11
|
+
> ```
|
|
12
|
+
>
|
|
13
|
+
> This command analyzes your CSS, Tailwind config, component files, and design tokens, then writes a complete DESIGN.md with structured YAML tokens and written design intent. It is safe to rerun any time your design system changes.
|