@smartmemory/compose 0.2.49-beta → 0.2.51-beta
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/skills/compose/SKILL.md +24 -1
- package/.claude/skills/compose/references/hermes-tools.md +80 -0
- package/README.md +1 -1
- package/bin/compose.js +340 -70
- package/dist/assets/App-CT0vXPgd.js +724 -0
- package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
- package/dist/assets/{arc-B1TqAC_R.js → arc-BXht3LyE.js} +1 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +132 -0
- package/dist/assets/{browser-DW2JlJCC.js → browser-CnKiSnlr.js} +6 -6
- package/dist/assets/{c4Diagram-AAUBKEIU-Dc6swoQo.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
- package/dist/assets/channel-Dd4XaiYv.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CdqXgd68.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
- package/dist/assets/{chunk-55IACEB6-D7VauOmU.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
- package/dist/assets/{chunk-2J33WTMH-6HKDSnOw.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C1Z5m8DP.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
- package/dist/assets/{chunk-QZHKN3VN-DY9GU2uD.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-DmqZMEQe.js → chunk-YZCP3GAM-DE6_5aqh.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +1 -0
- package/dist/assets/clone-CBsbNGAa.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B34g9xzV.js → cose-bilkent-S5V4N54A-DzzRyJtB.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +43 -0
- package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +24 -0
- package/dist/assets/{erDiagram-TEJ5UH35-C05_Iqmb.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-lXpnb-Ch.js → flowDiagram-DWJPFMVM-BzzO4JYU.js} +1 -1
- package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +106 -0
- package/dist/assets/graph-DZe55uk8.js +331 -0
- package/dist/assets/graph-Tq_bs_r0.js +1 -0
- package/dist/assets/index-8UhRLbGq.js +123 -0
- package/dist/assets/index-CRqB9els.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +2 -0
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CgdsWdgu.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
- package/dist/assets/{journeyDiagram-JHISSGLW-CyvHmE3z.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-Bqjdyv0T.js → kanban-definition-6JOO6SKY-CEq930ew.js} +1 -1
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-qtUgN9BC.js +1 -0
- package/dist/assets/{linear-DecnRXf0.js → linear-D158F7OT.js} +1 -1
- package/dist/assets/min-hESxN-c0.js +1 -0
- package/dist/assets/{mindmap-definition-RKZ34NQL-DM1x9vNY.js → mindmap-definition-QFDTVHPH-d9SSh2nG.js} +7 -7
- package/dist/assets/mobile-1gVCT0OK.css +1 -0
- package/dist/assets/mobile-Chw8RWyH.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +7 -0
- package/dist/assets/{requirementDiagram-4Y6WPE33-DD-Gg_y5.js → requirementDiagram-MS252O5E-B1ZFIotK.js} +1 -1
- package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-BPm2AEQj.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-DQCDWwlj.js → vennDiagram-DHZGUBPP-TZAW60b0.js} +4 -4
- package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +20 -0
- package/dist/assets/{xychartDiagram-2RQKCTM6-Ba05SXXF.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
- package/dist/index.html +4 -4
- package/lib/build-all.js +2 -1
- package/lib/build.js +363 -38
- package/lib/checkpoint/checkpoint-writer.js +3 -2
- package/lib/codex-preflight.js +151 -0
- package/lib/completion-writer.js +59 -15
- package/lib/feature-json.js +14 -3
- package/lib/feature-validator.js +27 -10
- package/lib/feature-write-guard.js +5 -7
- package/lib/feature-writer.js +3 -3
- package/lib/followup-writer.js +2 -2
- package/lib/get-roadmap.js +2 -2
- package/lib/gsd.js +2 -1
- package/lib/hooks-status.js +135 -0
- package/lib/ideabox.js +3 -2
- package/lib/install-agent-defs.js +22 -0
- package/lib/journal-writer.js +3 -3
- package/lib/migrate-anon.js +277 -0
- package/lib/migrate-roadmap.js +2 -3
- package/lib/paths-core.js +47 -0
- package/lib/project-paths.js +46 -37
- package/lib/roadmap-gen.js +3 -4
- package/lib/roadmap-graph/collect.js +5 -5
- package/lib/roadmap-graph/index.js +2 -2
- package/lib/roadmap-parser.js +1 -1
- package/lib/state-migrations.js +3 -3
- package/lib/test-bootstrap.js +165 -2
- package/lib/tracker/local-provider.js +2 -2
- package/lib/triage.js +7 -5
- package/lib/vocabulary-inject.js +103 -0
- package/lib/xref-push.js +4 -4
- package/lib/xref-sync.js +4 -4
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +433 -0
- package/pipelines/build.stratum.yaml +60 -3
- package/scripts/watch-server.sh +32 -0
- package/server/build-routes.js +89 -9
- package/server/build-stream-bridge.js +12 -0
- package/server/compose-mcp-tools.js +73 -1
- package/server/compose-mcp.js +27 -0
- package/server/design-routes.js +9 -4
- package/server/drift-axes.js +5 -3
- package/server/feature-scaffold-routes.js +82 -0
- package/server/feature-scan.js +8 -2
- package/server/file-watcher.js +13 -7
- package/server/health-routes.js +261 -0
- package/server/ideabox-routes.js +12 -11
- package/server/project-root.js +7 -4
- package/server/qa-scope-routes.js +96 -0
- package/server/session-routes.js +2 -2
- package/server/stratum-sync.js +85 -9
- package/server/validate-routes.js +70 -0
- package/server/vision-routes.js +46 -2
- package/server/vision-server.js +25 -3
- package/server/vision-utils.js +23 -4
- package/dist/assets/App-DOerhoDp.js +0 -706
- package/dist/assets/architectureDiagram-3BPJPVTR-C4hfxNOw.js +0 -36
- package/dist/assets/blockDiagram-GPEHLZMM-DRqkGncy.js +0 -132
- package/dist/assets/channel-KsY1x-7s.js +0 -1
- package/dist/assets/chunk-727SXJPM-CeKpXDif.js +0 -206
- package/dist/assets/chunk-AQP2D5EJ-y2XL-YHV.js +0 -231
- package/dist/assets/classDiagram-4FO5ZUOK-zDEDoVPk.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-zDEDoVPk.js +0 -1
- package/dist/assets/dagre-BM42HDAG-klZ_GOcw.js +0 -4
- package/dist/assets/diagram-2AECGRRQ-D0FmM6Wf.js +0 -43
- package/dist/assets/diagram-5GNKFQAL-DYuhby6q.js +0 -10
- package/dist/assets/diagram-KO2AKTUF-CCtQZgal.js +0 -3
- package/dist/assets/diagram-LMA3HP47-Cd1OqEln.js +0 -24
- package/dist/assets/diagram-OG6HWLK6-CiJrhoxo.js +0 -24
- package/dist/assets/ganttDiagram-6RSMTGT7-EPbL_2Hr.js +0 -292
- package/dist/assets/gitGraphDiagram-PVQCEYII-COCoF_vW.js +0 -106
- package/dist/assets/graph-BBXaecIU.js +0 -331
- package/dist/assets/graph-CAnANduQ.js +0 -1
- package/dist/assets/index-BwLfbbOu.css +0 -1
- package/dist/assets/index-DlgxA8dX.js +0 -123
- package/dist/assets/infoDiagram-5YYISTIA-B1sJw6j5.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DGIYPm2g.js +0 -1
- package/dist/assets/mobile-Cag5dHlF.css +0 -1
- package/dist/assets/mobile-DwmxS_O4.js +0 -17
- package/dist/assets/pieDiagram-4H26LBE5-CRtmCvwV.js +0 -30
- package/dist/assets/quadrantDiagram-W4KKPZXB-CsBLWg27.js +0 -7
- package/dist/assets/sankeyDiagram-5OEKKPKP-BPdMN8UU.js +0 -40
- package/dist/assets/sequenceDiagram-3UESZ5HK-D4AENdze.js +0 -162
- package/dist/assets/stateDiagram-AJRCARHV-DDplpCf_.js +0 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-J4t9AVzF.js +0 -1
- package/dist/assets/wardley-L42UT6IY-WERqOmJt.js +0 -173
- package/dist/assets/wardleyDiagram-YWT4CUSO-5teUSOiI.js +0 -78
package/lib/roadmap-gen.js
CHANGED
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
8
|
import { listFeatures, positionSortKey } from './feature-json.js';
|
|
10
9
|
import { parseStatusToken } from './roadmap-parser.js';
|
|
11
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
10
|
+
import { loadFeaturesDir, resolveRoadmapPath } from './project-paths.js';
|
|
12
11
|
import {
|
|
13
12
|
readPhaseOverrides,
|
|
14
13
|
readAnonymousRows,
|
|
@@ -207,7 +206,7 @@ export function generateRoadmapFromBase(baseText, features, opts = {}) {
|
|
|
207
206
|
* @returns {string} - Generated ROADMAP.md content
|
|
208
207
|
*/
|
|
209
208
|
export function generateRoadmap(cwd, opts = {}) {
|
|
210
|
-
const roadmapPath =
|
|
209
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
211
210
|
const existingText = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : '';
|
|
212
211
|
|
|
213
212
|
// Narrative-owned workspace: ROADMAP.md is hand-authored. Return it verbatim
|
|
@@ -513,7 +512,7 @@ function buildKeyDocs(features, featuresDir) {
|
|
|
513
512
|
* @param {object} [opts]
|
|
514
513
|
*/
|
|
515
514
|
export function writeRoadmap(cwd, opts = {}) {
|
|
516
|
-
const roadmapPath =
|
|
515
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
517
516
|
|
|
518
517
|
// Narrative-owned workspace: never overwrite hand-authored ROADMAP.md (#39).
|
|
519
518
|
// No-op + warn; the existing file on disk is the source of truth.
|
|
@@ -12,7 +12,7 @@ import { join, dirname, resolve } from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { parse as parseYaml } from 'yaml';
|
|
14
14
|
import { listFeatures } from '../feature-json.js';
|
|
15
|
-
import {
|
|
15
|
+
import { resolveFeaturesPath, resolveRoadmapPath, loadExternalPrefixes } from '../project-paths.js';
|
|
16
16
|
import { parseRoadmap } from '../roadmap-parser.js';
|
|
17
17
|
import { SchemaValidator } from '../../server/schema-validator.js';
|
|
18
18
|
import { depsToEdges } from './model.js';
|
|
@@ -28,7 +28,7 @@ const FM_KEYS = ['name', 'priority', 'track', 'desc'];
|
|
|
28
28
|
* @param {string} [featuresDir] relative features dir (default from compose.json)
|
|
29
29
|
* @returns {{ nodes: object[], rawEdges: object[], knownCodes: Set<string>, warnings: string[] }}
|
|
30
30
|
*/
|
|
31
|
-
export function collectGraphInputs(cwd, featuresDir =
|
|
31
|
+
export function collectGraphInputs(cwd, featuresDir = resolveFeaturesPath(cwd)) {
|
|
32
32
|
const warnings = [];
|
|
33
33
|
const depsValidator = new SchemaValidator(DEPS_SCHEMA);
|
|
34
34
|
const fmValidator = new SchemaValidator(FRONTMATTER_SCHEMA);
|
|
@@ -48,7 +48,7 @@ export function collectGraphInputs(cwd, featuresDir = loadFeaturesDir(cwd)) {
|
|
|
48
48
|
// An external-prefixed folder is a cross-project reference living here —
|
|
49
49
|
// known (so edges resolve) but never rendered as one of THIS project's nodes.
|
|
50
50
|
if (isExternal(f.code)) { externalCodes.add(f.code); continue; }
|
|
51
|
-
const folder = join(
|
|
51
|
+
const folder = join(featuresDir, f.code);
|
|
52
52
|
const fm = readDisplayMetadata(folder, f, fmValidator, warnings);
|
|
53
53
|
universe.set(f.code, {
|
|
54
54
|
id: f.code,
|
|
@@ -62,7 +62,7 @@ export function collectGraphInputs(cwd, featuresDir = loadFeaturesDir(cwd)) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// (b) ROADMAP.md rows — fallback for features not registered as folders.
|
|
65
|
-
const roadmapPath =
|
|
65
|
+
const roadmapPath = resolveRoadmapPath(cwd);
|
|
66
66
|
if (existsSync(roadmapPath)) {
|
|
67
67
|
let entries = [];
|
|
68
68
|
try { entries = parseRoadmap(readFileSync(roadmapPath, 'utf-8')); } catch { /* ignore */ }
|
|
@@ -88,7 +88,7 @@ export function collectGraphInputs(cwd, featuresDir = loadFeaturesDir(cwd)) {
|
|
|
88
88
|
for (const f of features) {
|
|
89
89
|
if (!f || typeof f.code !== 'string') continue;
|
|
90
90
|
if (isExternal(f.code)) continue;
|
|
91
|
-
const depsPath = join(
|
|
91
|
+
const depsPath = join(featuresDir, f.code, 'deps.yaml');
|
|
92
92
|
if (!existsSync(depsPath)) continue;
|
|
93
93
|
let deps;
|
|
94
94
|
try {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
12
12
|
import { join, isAbsolute, dirname } from 'node:path';
|
|
13
|
-
import {
|
|
13
|
+
import { resolveFeaturesPath } from '../project-paths.js';
|
|
14
14
|
import { collectGraphInputs } from './collect.js';
|
|
15
15
|
import { buildGraph } from './model.js';
|
|
16
16
|
import { renderGraphHtml } from './render.js';
|
|
@@ -22,7 +22,7 @@ import { loadGraphConfig } from './config.js';
|
|
|
22
22
|
* @returns {{ html: string, nodes: object[], edges: object[], dropped: string[], warnings: string[], config: object }}
|
|
23
23
|
*/
|
|
24
24
|
export function buildRoadmapGraph(cwd) {
|
|
25
|
-
const featuresDir =
|
|
25
|
+
const featuresDir = resolveFeaturesPath(cwd);
|
|
26
26
|
const inputs = collectGraphInputs(cwd, featuresDir);
|
|
27
27
|
const graph = buildGraph(inputs); // throws DanglingEdgeError
|
|
28
28
|
const config = loadGraphConfig(cwd);
|
package/lib/roadmap-parser.js
CHANGED
|
@@ -192,7 +192,7 @@ export function parseRoadmap(text) {
|
|
|
192
192
|
* 3-col: ID | Item | Status → code=ID, desc=Item
|
|
193
193
|
* 3-col: # | Item | Status → anonymous (# is a row number, not a code)
|
|
194
194
|
*/
|
|
195
|
-
function detectColumnLayout(headerCells) {
|
|
195
|
+
export function detectColumnLayout(headerCells) {
|
|
196
196
|
const lower = headerCells.map(c => c.toLowerCase());
|
|
197
197
|
|
|
198
198
|
// 4+ columns: look for a Feature column as the code source
|
package/lib/state-migrations.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
renameSync, unlinkSync, readdirSync,
|
|
28
28
|
} from 'fs';
|
|
29
29
|
import { join } from 'path';
|
|
30
|
-
import {
|
|
30
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
31
31
|
import { writeFeature } from './feature-json.js';
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -283,8 +283,8 @@ export function runStateMigrations(cwd, opts = {}) {
|
|
|
283
283
|
|
|
284
284
|
// --- feature.json walk -----------------------------------------------------
|
|
285
285
|
if (featurePending.length > 0) {
|
|
286
|
-
const featuresDir =
|
|
287
|
-
const featuresRoot =
|
|
286
|
+
const featuresDir = resolveFeaturesPath(cwd); // absolute; honors paths.features override
|
|
287
|
+
const featuresRoot = featuresDir;
|
|
288
288
|
// Own directory walk — do NOT use listFeatures(): it silently skips unreadable
|
|
289
289
|
// files, which would hide exactly the cold-data corruption we must surface.
|
|
290
290
|
let dirs = [];
|
package/lib/test-bootstrap.js
CHANGED
|
@@ -119,7 +119,10 @@ export function detectTestFramework(cwd) {
|
|
|
119
119
|
framework: 'go-test',
|
|
120
120
|
runner: 'go',
|
|
121
121
|
configFile: 'go.mod',
|
|
122
|
-
|
|
122
|
+
// -v emits per-test "--- PASS:/FAIL:" verdicts so parseTestSummary can
|
|
123
|
+
// count tests (COMP-TEST-BOOTSTRAP-4); plain `go test` prints only a
|
|
124
|
+
// package-level ok/FAIL with no counts.
|
|
125
|
+
command: 'go test -v ./...',
|
|
123
126
|
};
|
|
124
127
|
}
|
|
125
128
|
}
|
|
@@ -298,7 +301,8 @@ export function scaffoldTestFramework(cwd, language) {
|
|
|
298
301
|
framework: 'go-test',
|
|
299
302
|
configFile: 'golden_test.go',
|
|
300
303
|
testDir: '.',
|
|
301
|
-
|
|
304
|
+
// -v so parseTestSummary can count per-test verdicts (COMP-TEST-BOOTSTRAP-4).
|
|
305
|
+
command: 'go test -v ./...',
|
|
302
306
|
};
|
|
303
307
|
}
|
|
304
308
|
|
|
@@ -399,3 +403,162 @@ export function scaffoldTestFramework(cwd, language) {
|
|
|
399
403
|
}
|
|
400
404
|
}
|
|
401
405
|
}
|
|
406
|
+
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// COMP-TEST-BOOTSTRAP-4-1: isTestFile
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Whether a repo-relative path looks like a test file. Used to scope the
|
|
413
|
+
* post-coverage test-review pass to the tests the coverage step produced.
|
|
414
|
+
* Conservative: matches conventional test locations/suffixes across the
|
|
415
|
+
* frameworks detectTestFramework knows, and avoids substring false positives
|
|
416
|
+
* (e.g. "contest", "greatest").
|
|
417
|
+
*
|
|
418
|
+
* @param {string} p - repo-relative file path
|
|
419
|
+
* @returns {boolean}
|
|
420
|
+
*/
|
|
421
|
+
export function isTestFile(p) {
|
|
422
|
+
if (typeof p !== 'string' || p.length === 0) return false;
|
|
423
|
+
return (
|
|
424
|
+
/(^|\/)(tests?|__tests__|spec)\//.test(p) || // test/ tests/ __tests__/ spec/ dirs
|
|
425
|
+
/\.(test|spec)\.[mc]?[jt]sx?$/.test(p) || // *.test.* / *.spec.* (js/ts/jsx/tsx/mjs/cjs)
|
|
426
|
+
/_test\.go$/.test(p) || // go
|
|
427
|
+
/(^|\/)test_[^/]*\.py$/.test(p) || // pytest test_*.py
|
|
428
|
+
/_test\.py$/.test(p) // pytest *_test.py
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
// COMP-TEST-BOOTSTRAP-4: parseTestSummary
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
const _UNPARSED = Object.freeze({ test_count: 0, pass_rate: 0, parsed: false });
|
|
437
|
+
|
|
438
|
+
/** Strip ANSI color/escape codes so regexes match raw text. */
|
|
439
|
+
function _stripAnsi(s) {
|
|
440
|
+
// eslint-disable-next-line no-control-regex
|
|
441
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** First capture group as an int, or null if no match. */
|
|
445
|
+
function _int(m) {
|
|
446
|
+
return m ? parseInt(m[1], 10) : null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Build a result from pass/fail counts. The denominator is passed+failed only —
|
|
451
|
+
* skipped/pending/ignored tests are excluded from the rate (a fully-skipped run
|
|
452
|
+
* is not "100% passing"; a run with zero verdicts degrades to parsed:false).
|
|
453
|
+
*/
|
|
454
|
+
function _fromCounts(passed, failed) {
|
|
455
|
+
const total = passed + failed;
|
|
456
|
+
if (total <= 0) return _UNPARSED;
|
|
457
|
+
// Round to 2 decimals; an all-pass run yields exactly 100.
|
|
458
|
+
const pass_rate = Math.round((passed / total) * 10000) / 100;
|
|
459
|
+
return { test_count: total, pass_rate, parsed: true };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function _parseVitest(out) {
|
|
463
|
+
// " Tests 2 failed | 10 passed (12)" / " Tests 12 passed (12)"
|
|
464
|
+
// Match the "Tests" line specifically, not the "Test Files" line.
|
|
465
|
+
const line = out.match(/^[^\S\n]*Tests[^\S\n]+(.+)$/m);
|
|
466
|
+
if (!line) return _UNPARSED;
|
|
467
|
+
const passed = _int(line[1].match(/(\d+)\s+passed/));
|
|
468
|
+
const failed = _int(line[1].match(/(\d+)\s+failed/));
|
|
469
|
+
if (passed === null && failed === null) return _UNPARSED;
|
|
470
|
+
return _fromCounts(passed ?? 0, failed ?? 0);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function _parseJest(out) {
|
|
474
|
+
// "Tests: 2 failed, 10 passed, 12 total"
|
|
475
|
+
const line = out.match(/^[^\S\n]*Tests:[^\S\n]+(.+)$/m);
|
|
476
|
+
if (!line) return _UNPARSED;
|
|
477
|
+
const passed = _int(line[1].match(/(\d+)\s+passed/));
|
|
478
|
+
const failed = _int(line[1].match(/(\d+)\s+failed/));
|
|
479
|
+
if (passed === null && failed === null) return _UNPARSED;
|
|
480
|
+
return _fromCounts(passed ?? 0, failed ?? 0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function _parseMocha(out) {
|
|
484
|
+
// " 10 passing (24ms)" + optional " 2 failing"; pending excluded.
|
|
485
|
+
const passed = _int(out.match(/(\d+)\s+passing/));
|
|
486
|
+
const failed = _int(out.match(/(\d+)\s+failing/));
|
|
487
|
+
if (passed === null && failed === null) return _UNPARSED;
|
|
488
|
+
return _fromCounts(passed ?? 0, failed ?? 0);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function _parsePytest(out) {
|
|
492
|
+
// "===== 2 failed, 9 passed, 1 error, 1 skipped in 0.50s =====" / "12 passed in 0.34s"
|
|
493
|
+
const passed = _int(out.match(/(\d+)\s+passed/));
|
|
494
|
+
const failed = _int(out.match(/(\d+)\s+failed/));
|
|
495
|
+
const errors = _int(out.match(/(\d+)\s+error/)); // "error"/"errors"
|
|
496
|
+
if (passed === null && failed === null && errors === null) return _UNPARSED;
|
|
497
|
+
return _fromCounts(passed ?? 0, (failed ?? 0) + (errors ?? 0));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function _parseGoTest(out) {
|
|
501
|
+
// Requires verbose output (`go test -v`): per-test "--- PASS:" / "--- FAIL:" lines.
|
|
502
|
+
// Without -v there are no per-test verdicts to count → degrade.
|
|
503
|
+
const passed = (out.match(/^[^\S\n]*--- PASS:/gm) || []).length;
|
|
504
|
+
const failed = (out.match(/^[^\S\n]*--- FAIL:/gm) || []).length;
|
|
505
|
+
if (passed + failed === 0) return _UNPARSED;
|
|
506
|
+
return _fromCounts(passed, failed);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function _parseCargo(out) {
|
|
510
|
+
// Sum every "test result: <ok|FAILED>. N passed; M failed; ..." line
|
|
511
|
+
// (a crate emits one per unit/integration/doc target).
|
|
512
|
+
const re = /test result:\s+\w+\.\s+(\d+)\s+passed;\s+(\d+)\s+failed/g;
|
|
513
|
+
let passed = 0, failed = 0, found = false, m;
|
|
514
|
+
while ((m = re.exec(out)) !== null) {
|
|
515
|
+
passed += parseInt(m[1], 10);
|
|
516
|
+
failed += parseInt(m[2], 10);
|
|
517
|
+
found = true;
|
|
518
|
+
}
|
|
519
|
+
if (!found) return _UNPARSED;
|
|
520
|
+
return _fromCounts(passed, failed);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Extract a structured test-result summary from raw test-runner stdout.
|
|
525
|
+
*
|
|
526
|
+
* Pure and total: any framework it can't read — or output it can't recognize —
|
|
527
|
+
* degrades to `{ test_count: 0, pass_rate: 0, parsed: false }` rather than
|
|
528
|
+
* guessing. Callers MUST treat `parsed:false` as "no signal" and never as a
|
|
529
|
+
* failing gate (see lib/build.js ship path).
|
|
530
|
+
*
|
|
531
|
+
* @param {string|null} framework - value from detectTestFramework().framework
|
|
532
|
+
* (vitest | jest | mocha | pytest | go-test | cargo-test; others degrade)
|
|
533
|
+
* @param {string|null} stdout - combined stdout+stderr of the test run
|
|
534
|
+
* @returns {{ test_count: number, pass_rate: number, parsed: boolean }}
|
|
535
|
+
*/
|
|
536
|
+
export function parseTestSummary(framework, stdout) {
|
|
537
|
+
if (!framework || typeof stdout !== 'string' || stdout.length === 0) return _UNPARSED;
|
|
538
|
+
const out = _stripAnsi(stdout);
|
|
539
|
+
switch (framework) {
|
|
540
|
+
case 'vitest': return _parseVitest(out);
|
|
541
|
+
case 'jest': return _parseJest(out);
|
|
542
|
+
case 'mocha': return _parseMocha(out);
|
|
543
|
+
case 'pytest': return _parsePytest(out);
|
|
544
|
+
case 'go-test': return _parseGoTest(out);
|
|
545
|
+
case 'cargo-test': return _parseCargo(out);
|
|
546
|
+
default: return _UNPARSED; // ava, tap, unknown
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* The ship gate: derive a `tests_pass` boolean from a parsed summary.
|
|
552
|
+
*
|
|
553
|
+
* - parsed: pass only when at least one test ran AND every test passed.
|
|
554
|
+
* - unparsed (or absent): degrade to `true` — an unreadable summary is "no
|
|
555
|
+
* signal", never a failing gate. This is the safety valve that keeps the
|
|
556
|
+
* gate from misfiring on frameworks/output the parser can't read.
|
|
557
|
+
*
|
|
558
|
+
* @param {{ test_count: number, pass_rate: number, parsed: boolean }|null} summary
|
|
559
|
+
* @returns {boolean}
|
|
560
|
+
*/
|
|
561
|
+
export function deriveTestsPass(summary) {
|
|
562
|
+
if (!summary || !summary.parsed) return true;
|
|
563
|
+
return summary.test_count >= 1 && summary.pass_rate === 100;
|
|
564
|
+
}
|
|
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, renameS
|
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
|
|
4
4
|
import { readFeature, listFeatures as listFeaturesRaw, writeFeature } from '../feature-json.js';
|
|
5
|
-
import {
|
|
5
|
+
import { resolveFeaturesPath } from '../project-paths.js';
|
|
6
6
|
import { TrackerProvider, CAP } from './provider.js';
|
|
7
7
|
|
|
8
8
|
import { setFeatureStatus, addRoadmapEntry as addRoadmapEntryRaw } from '../feature-writer.js';
|
|
@@ -38,7 +38,7 @@ export class LocalFileProvider extends TrackerProvider {
|
|
|
38
38
|
|
|
39
39
|
async init(cwd) {
|
|
40
40
|
this.cwd = cwd;
|
|
41
|
-
this.featuresDir =
|
|
41
|
+
this.featuresDir = resolveFeaturesPath(cwd);
|
|
42
42
|
return this;
|
|
43
43
|
}
|
|
44
44
|
|
package/lib/triage.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
15
|
+
import { resolvePathValue } from './paths-core.js';
|
|
14
16
|
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Tier definitions
|
|
@@ -185,8 +187,8 @@ function deriveProfile(signals) {
|
|
|
185
187
|
*/
|
|
186
188
|
export async function runTriage(featureCode, opts = {}) {
|
|
187
189
|
const cwd = opts.cwd ?? process.cwd();
|
|
188
|
-
const featuresDir = opts.featuresDir ??
|
|
189
|
-
const featureDir = join(cwd, featuresDir, featureCode);
|
|
190
|
+
const featuresDir = opts.featuresDir ?? resolveFeaturesPath(cwd);
|
|
191
|
+
const featureDir = join(resolvePathValue(cwd, featuresDir, 'features'), featureCode);
|
|
190
192
|
|
|
191
193
|
// Collect content from key files
|
|
192
194
|
const candidateFiles = ['plan.md', 'blueprint.md', 'design.md', 'prd.md', 'architecture.md'];
|
|
@@ -234,11 +236,11 @@ export async function runTriage(featureCode, opts = {}) {
|
|
|
234
236
|
*
|
|
235
237
|
* @param {string} cwd - Project root
|
|
236
238
|
* @param {string} featureCode - Feature code
|
|
237
|
-
* @param {string} [featuresDir] -
|
|
239
|
+
* @param {string} [featuresDir] - Path to features dir (absolute or relative; default: resolved features path)
|
|
238
240
|
* @returns {boolean}
|
|
239
241
|
*/
|
|
240
|
-
export function isTriageStale(cwd, featureCode, featuresDir =
|
|
241
|
-
const featureDir = join(cwd, featuresDir, featureCode);
|
|
242
|
+
export function isTriageStale(cwd, featureCode, featuresDir = resolveFeaturesPath(cwd)) {
|
|
243
|
+
const featureDir = join(resolvePathValue(cwd, featuresDir, 'features'), featureCode);
|
|
242
244
|
const featureJsonPath = join(featureDir, 'feature.json');
|
|
243
245
|
|
|
244
246
|
if (!existsSync(featureJsonPath)) return true;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STRAT-VOCAB-3 — Compose integration for vocabulary enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Wires the already-shipped Stratum `vocabulary_compliance` ensure builtin
|
|
5
|
+
* (stratum-mcp/src/stratum_mcp/spec.py) into the compose lifecycle:
|
|
6
|
+
* - VOCABULARY_TEMPLATE : starter contracts/vocabulary.yaml (compose init)
|
|
7
|
+
* - vocabularyEnabled() : gate — capability not disabled AND a vocab file exists
|
|
8
|
+
* - injectVocabularyEnsure() : append the vocab ensure to the build flow's `review` step
|
|
9
|
+
* - tagVocabularyViolations() : mark vocab violation strings as must-fix for display
|
|
10
|
+
*
|
|
11
|
+
* Design: docs/features/STRAT-VOCAB-3/design.md
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
/** Project-relative location of the vocabulary file (also the path baked into the ensure). */
|
|
17
|
+
export const VOCABULARY_FILE = 'contracts/vocabulary.yaml';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The ensure expression injected into the build flow's `review` step.
|
|
21
|
+
* Empty-list literal + git_fallback=True: never references a possibly-missing
|
|
22
|
+
* `result.*` attribute; scans the uncommitted working-tree diff vs HEAD (the
|
|
23
|
+
* implementation changes are merged but not yet committed at review time).
|
|
24
|
+
* Python-evaluated by the Stratum executor — hence `[]` and `True`.
|
|
25
|
+
*/
|
|
26
|
+
export const VOCABULARY_ENSURE = `vocabulary_compliance('${VOCABULARY_FILE}', [], True)`;
|
|
27
|
+
|
|
28
|
+
/** Starter vocabulary file: all comments, so `_load_vocabulary` returns {} (inert) until edited. */
|
|
29
|
+
export const VOCABULARY_TEMPLATE = `# contracts/vocabulary.yaml — project naming vocabulary (STRAT-VOCAB)
|
|
30
|
+
#
|
|
31
|
+
# Declare canonical names and the aliases that must NOT appear in code. During
|
|
32
|
+
# \`compose build\`, the review step scans the files changed by the build for any
|
|
33
|
+
# rejected alias (whole-word, case-sensitive) and fails until each is replaced
|
|
34
|
+
# with its canonical name. An empty / comments-only file is a no-op.
|
|
35
|
+
#
|
|
36
|
+
# Format — a flat map of canonical_name -> { reject: [...], reason: "..." }:
|
|
37
|
+
#
|
|
38
|
+
# auth_token:
|
|
39
|
+
# reject: [jwt, accessToken, JwtToken, authToken]
|
|
40
|
+
# reason: "use auth_token everywhere for the session credential"
|
|
41
|
+
#
|
|
42
|
+
# user_id:
|
|
43
|
+
# reject: [uid, userId, UserId]
|
|
44
|
+
#
|
|
45
|
+
# Rules: canonical names and aliases must be identifiers; an alias may not also
|
|
46
|
+
# be a canonical name or be repeated across entries; \`reason\` is optional.
|
|
47
|
+
# Uncomment and edit the examples above (or add your own) to enable enforcement.
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Is vocabulary enforcement active for this project?
|
|
52
|
+
* Default-ON (honoring the roadmap's "by default") but gated on the file existing,
|
|
53
|
+
* so the generated spec is byte-identical for any project without a vocab file.
|
|
54
|
+
* Opt out with capabilities.vocabularyCompliance === false.
|
|
55
|
+
*/
|
|
56
|
+
export function vocabularyEnabled(cwd, composeConfig) {
|
|
57
|
+
if (composeConfig?.capabilities?.vocabularyCompliance === false) return false;
|
|
58
|
+
return existsSync(join(cwd, VOCABULARY_FILE));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Append the vocabulary ensure to the EXECUTED flow's `review` step (idempotent).
|
|
63
|
+
* Mutates and returns specObj. No-op when that flow has no `review` step.
|
|
64
|
+
*
|
|
65
|
+
* `flowName` must be the flow Stratum will actually run (build.js resolves it via
|
|
66
|
+
* extractFlowName). Targeting it precisely matters because a sub-flow
|
|
67
|
+
* (`review_check`) ALSO has a step id'd `review`; injecting there would be wrong.
|
|
68
|
+
* When `flowName` is omitted/unknown, fall back to the `build` flow (or the first),
|
|
69
|
+
* matching the shipped templates.
|
|
70
|
+
*/
|
|
71
|
+
export function injectVocabularyEnsure(specObj, flowName) {
|
|
72
|
+
const flows = specObj?.flows ?? {};
|
|
73
|
+
const keys = Object.keys(flows);
|
|
74
|
+
const flowKey = flowName && keys.includes(flowName)
|
|
75
|
+
? flowName
|
|
76
|
+
: (keys.includes('build') ? 'build' : keys[0]);
|
|
77
|
+
const steps = flows[flowKey]?.steps ?? [];
|
|
78
|
+
const review = steps.find((s) => s?.id === 'review');
|
|
79
|
+
if (!review) return specObj;
|
|
80
|
+
if (!Array.isArray(review.ensure)) review.ensure = [];
|
|
81
|
+
if (!review.ensure.includes(VOCABULARY_ENSURE)) review.ensure.push(VOCABULARY_ENSURE);
|
|
82
|
+
return specObj;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Tag vocabulary failure strings as must-fix for the findings display.
|
|
87
|
+
* The cli-progress parser classifies a string by keyword (defaults to `nit`);
|
|
88
|
+
* vocab failures carry no marker, so prefix them with `must-fix:` — the parser
|
|
89
|
+
* then classifies must-fix AND still extracts the file:line, and strips the
|
|
90
|
+
* prefix for a clean description. Non-string / non-vocab items pass through
|
|
91
|
+
* unchanged. Returns a new array (does not mutate input or stored violations).
|
|
92
|
+
*
|
|
93
|
+
* Matches every string the builtin emits — both alias hits
|
|
94
|
+
* ("vocabulary violation: …") and a broken vocab file
|
|
95
|
+
* ("vocabulary.yaml malformed: …" / "vocabulary.yaml schema error: …") — all of
|
|
96
|
+
* which block the step and so deserve must-fix, not nit.
|
|
97
|
+
*/
|
|
98
|
+
export function tagVocabularyViolations(violations) {
|
|
99
|
+
if (!Array.isArray(violations)) return violations;
|
|
100
|
+
return violations.map((v) =>
|
|
101
|
+
typeof v === 'string' && /^vocabulary[ .]/i.test(v) ? `must-fix: ${v}` : v
|
|
102
|
+
);
|
|
103
|
+
}
|
package/lib/xref-push.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { readdirSync, existsSync, readFileSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
|
-
import {
|
|
20
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
21
21
|
import { resolveSiblingRoot } from './xref-local.js';
|
|
22
22
|
|
|
23
23
|
const GITHUB_STATES = new Set(['open', 'closed']);
|
|
@@ -145,7 +145,7 @@ function localResolve(link, cwd) {
|
|
|
145
145
|
const sib = resolveSiblingRoot(cwd, link.repo);
|
|
146
146
|
if (sib.skipped) return sib;
|
|
147
147
|
try {
|
|
148
|
-
const fjPath = join(
|
|
148
|
+
const fjPath = join(resolveFeaturesPath(sib.root), link.to_code, 'feature.json');
|
|
149
149
|
if (!existsSync(fjPath)) return { skipped: true, reason: `local target ${link.repo}/${link.to_code} not found` };
|
|
150
150
|
return { state: JSON.parse(readFileSync(fjPath, 'utf8')).status || null, root: sib.root };
|
|
151
151
|
} catch (e) { return { skipped: true, reason: `unreadable local target: ${e.message}` }; }
|
|
@@ -171,13 +171,13 @@ function localResolve(link, cwd) {
|
|
|
171
171
|
* @returns {Promise<{pushed: Array, skipped: Array, unchanged: number, scanned: number}>}
|
|
172
172
|
*/
|
|
173
173
|
export async function pushExternalRefs(cwd, opts = {}) {
|
|
174
|
-
const featuresDir = opts.featuresDir ??
|
|
174
|
+
const featuresDir = opts.featuresDir ?? resolveFeaturesPath(cwd);
|
|
175
175
|
const clientOpts = { transport: opts.githubTransport ?? null, auth: opts.githubAuth };
|
|
176
176
|
const ghResolve = opts.resolve ?? ((link) => defaultResolve(link, clientOpts));
|
|
177
177
|
const ghWrite = opts.write ?? ((link, patch) => defaultWrite(link, patch, clientOpts));
|
|
178
178
|
const setStatus = opts.setStatus ?? defaultSetStatus;
|
|
179
179
|
const apply = opts.apply === true;
|
|
180
|
-
const dir =
|
|
180
|
+
const dir = featuresDir;
|
|
181
181
|
|
|
182
182
|
const pushed = [];
|
|
183
183
|
const skipped = [];
|
package/lib/xref-sync.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { readdirSync, existsSync, readFileSync } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { writeFeature } from './feature-json.js';
|
|
19
|
-
import {
|
|
19
|
+
import { resolveFeaturesPath } from './project-paths.js';
|
|
20
20
|
import { resolveSiblingRoot } from './xref-local.js';
|
|
21
21
|
|
|
22
22
|
const RESOLVABLE = new Set(['github', 'local']);
|
|
@@ -80,7 +80,7 @@ async function defaultResolve(link, cwd, featuresDir) {
|
|
|
80
80
|
const citedRoot = sib.root;
|
|
81
81
|
// Resolve the SIBLING's own features dir (it may have its own paths.features).
|
|
82
82
|
try {
|
|
83
|
-
const fjPath = join(
|
|
83
|
+
const fjPath = join(resolveFeaturesPath(citedRoot), link.to_code, 'feature.json');
|
|
84
84
|
if (!existsSync(fjPath)) return { skipped: true, reason: `local target ${link.repo}/${link.to_code} not found` };
|
|
85
85
|
return { state: JSON.parse(readFileSync(fjPath, 'utf8')).status || null };
|
|
86
86
|
} catch (e) { return { skipped: true, reason: `unreadable local target: ${e.message}` }; }
|
|
@@ -100,9 +100,9 @@ async function defaultResolve(link, cwd, featuresDir) {
|
|
|
100
100
|
* @returns {Promise<{synced: Array, skipped: Array, unchanged: number, scanned: number}>}
|
|
101
101
|
*/
|
|
102
102
|
export async function syncExternalRefs(cwd, opts = {}) {
|
|
103
|
-
const featuresDir = opts.featuresDir ??
|
|
103
|
+
const featuresDir = opts.featuresDir ?? resolveFeaturesPath(cwd);
|
|
104
104
|
const resolve = opts.resolve ?? defaultResolve;
|
|
105
|
-
const dir =
|
|
105
|
+
const dir = featuresDir;
|
|
106
106
|
|
|
107
107
|
const synced = [];
|
|
108
108
|
const skipped = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.51-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"dev": "node server/supervisor.js 2>&1 | tee /tmp/compose-server.log",
|
|
17
17
|
"dev:server": "node server/supervisor.js",
|
|
18
18
|
"dev:server:bare": "node server/index.js",
|
|
19
|
+
"dev:watch": "bash scripts/watch-server.sh",
|
|
19
20
|
"dev:client": "vite",
|
|
20
21
|
"build": "vite build",
|
|
21
22
|
"preview": "vite preview",
|