@cspeach/cli 1.1.30 → 1.1.32
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/README.md +11 -1
- package/dist/agent/loop.js +132 -9
- package/dist/cli.js +49 -18
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/help.js +16 -1
- package/dist/commands/plan-chain.js +3 -0
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +17 -1
- package/dist/commands/plan-resume.js +28 -18
- package/dist/commands/plan-startup.js +49 -6
- package/dist/commands/team-discover.js +195 -0
- package/dist/commands/team-plan.js +585 -0
- package/dist/commands/team-rules.js +187 -0
- package/dist/commands/team.js +971 -95
- package/dist/journey/chain.js +978 -0
- package/dist/journey/clean-core.js +235 -0
- package/dist/journey/flow.js +216 -0
- package/dist/journey/rebuild.js +464 -0
- package/dist/journey/run.js +628 -0
- package/dist/journey/session-deps.js +81 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +21 -9
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +28 -2
- package/dist/projects/clean-core-confirm.js +192 -3
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +3 -0
- package/dist/projects/migration.js +4 -105
- package/dist/projects/plan-lane.js +67 -17
- package/dist/projects/plan-sources.js +13 -7
- package/dist/projects/project-folder.js +131 -0
- package/dist/projects/promote-command.js +39 -0
- package/dist/projects/save-command.js +100 -12
- package/dist/projects/saved-lines.js +17 -3
- package/dist/projects/sort-command.js +499 -0
- package/dist/projects/sort-core.js +248 -0
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -121
- package/dist/register/baseline.js +39 -2
- package/dist/register/decision-rules.js +35 -0
- package/dist/register/discover.js +931 -0
- package/dist/register/export-files.js +58 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/measure.js +191 -9
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +328 -0
- package/dist/register/records.js +40 -3
- package/dist/register/rule-proposal.js +4 -0
- package/dist/register/rules-edit.js +257 -0
- package/dist/register/status-file/estate-parts.js +168 -0
- package/dist/register/status-file/plan-parts.js +68 -0
- package/dist/register/status-file/progress-chart.js +110 -0
- package/dist/register/status-file/register-map.js +11 -3
- package/dist/register/status-file/register-model.js +40 -6
- package/dist/register/status-file/register-parts.js +57 -11
- package/dist/register/status-file/register-status-file.js +106 -90
- package/dist/register/status-file/report-model.js +569 -0
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +4 -0
- package/dist/register/status-html.js +27 -6
- package/dist/register/store.js +44 -5
- package/dist/register/variant.js +65 -0
- package/dist/register/workbooks.js +28 -42
- package/dist/register/write-whole.js +30 -2
- package/dist/register/xlsx.js +13 -3
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/renderer/tool-widget.js +15 -1
- package/dist/repl/at-completer.js +6 -2
- package/dist/repl/at-picker.js +1 -1
- package/dist/repl/builtin-commands.js +4 -0
- package/dist/repl/journey-entry.js +27 -0
- package/dist/repl/plan-hop.js +2 -9
- package/dist/repl/rules-menu.js +177 -0
- package/dist/repl/slash-completer.js +1 -0
- package/dist/repl/slash-picker.js +5 -4
- package/dist/repl.js +236 -59
- package/dist/router/classifier.js +8 -4
- package/dist/router/router-core.js +43 -7
- package/dist/router/routing-decision.js +1 -1
- package/dist/runs/bundled-runs.js +100 -0
- package/dist/runs/front-door.js +19 -0
- package/dist/runs/manifest-runs.js +50 -0
- package/dist/runs/run-command.js +24 -0
- package/dist/runs/runs.js +27 -0
- package/dist/runs/shadow.js +100 -0
- package/dist/runs/types.js +1 -0
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/queued-step.js +45 -0
- package/dist/session/resume.js +72 -24
- package/dist/session/schema.js +3 -1
- package/dist/session/store.js +96 -51
- package/dist/skill-catalog.js +2 -2
- package/dist/skills/bundled-skills.js +53 -53
- package/dist/tools/_filesystem-shared.js +174 -12
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/dispatch-skill.js +8 -4
- package/dist/tools/filesystem/extract-xlsx.js +27 -3
- package/dist/tools/filesystem/file-edit.js +12 -4
- package/dist/tools/filesystem/file-read.js +8 -4
- package/dist/tools/filesystem/file-write.js +12 -4
- package/dist/tools/filesystem/glob.js +33 -8
- package/dist/tools/filesystem/grep.js +9 -4
- package/dist/tools/filesystem/read-document.js +6 -3
- package/dist/tools/lane/app-lane-resolve.js +1 -1
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/rules_propose.js +59 -0
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +89 -5
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +200 -10
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/transport-fit-check.js +117 -0
- package/dist/tools/transport-fit.js +15 -7
- package/dist/tools/transport.js +57 -2
- package/dist/ui/app.js +2 -2
- package/dist/ui/approval-modal.js +74 -6
- package/dist/ui/widgets/confirm-card.js +68 -5
- package/dist/upgrade-check.js +49 -4
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +3 -3
|
@@ -7,56 +7,23 @@
|
|
|
7
7
|
* the user — the path-copy circus. The workspace folder is one configurable
|
|
8
8
|
* directory that the CLI saves into and resolves bare @<name> tokens against.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 2. `<git-root>/.cspeach/` — when the cwd is inside a git repository.
|
|
14
|
-
* Each customer / project gets its own workspace; multi-project
|
|
15
|
-
* consultants don't have artefacts piling up in one folder.
|
|
16
|
-
* 3. `<cwd>/.cspeach/` — fallback when cwd isn't inside a git repo.
|
|
17
|
-
*
|
|
18
|
-
* Earlier (pre-2026-05-08) default was `~/CSPeach/`. A first-run migration
|
|
19
|
-
* prompt (see src/projects/migration.ts) offers to move existing artefacts
|
|
20
|
-
* into the new project-rooted workspace, or pin workspace_folder back to
|
|
21
|
-
* `~/CSPeach/` for users who prefer the old behaviour.
|
|
10
|
+
* Resolution (2026-10-08): see projects/project-folder.ts — the folder you
|
|
11
|
+
* start in; team folders save to the seat. No git walk; workspace_folder is
|
|
12
|
+
* ignored.
|
|
22
13
|
*
|
|
23
14
|
* The folder is created on first use — no `cspeach init` ceremony.
|
|
24
15
|
*/
|
|
25
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
26
17
|
import { dirname, join, isAbsolute } from 'node:path';
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
import { configFile } from '../config/paths.js';
|
|
18
|
+
import { legacyDirsFor, resolveProjectFolderSync } from './project-folder.js';
|
|
19
|
+
import { cspeachRoot } from '../config/paths.js';
|
|
20
|
+
import { normDir } from '../session/project-match.js';
|
|
31
21
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* containing `.git`, or null when the search hits the filesystem root
|
|
35
|
-
* without finding one.
|
|
36
|
-
*
|
|
37
|
-
* Bounded by parent === dir, which terminates at "/" on POSIX and at
|
|
38
|
-
* each drive root on Windows.
|
|
39
|
-
*/
|
|
40
|
-
export function findProjectRoot(startDir) {
|
|
41
|
-
let dir = startDir;
|
|
42
|
-
for (let i = 0; i < 200; i++) {
|
|
43
|
-
if (existsSync(join(dir, '.git')))
|
|
44
|
-
return dir;
|
|
45
|
-
const parent = dirname(dir);
|
|
46
|
-
if (parent === dir)
|
|
47
|
-
return null;
|
|
48
|
-
dir = parent;
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Resolve the project-rooted default workspace path for a given cwd.
|
|
54
|
-
* Pure: does NOT touch the filesystem (other than the .git existence
|
|
55
|
-
* check via findProjectRoot). Caller decides whether to mkdir.
|
|
22
|
+
* The workspace path for a given cwd (see projects/project-folder.ts).
|
|
23
|
+
* Pure: does NOT create anything. Caller decides whether to mkdir.
|
|
56
24
|
*/
|
|
57
25
|
export function defaultWorkspaceFor(cwd) {
|
|
58
|
-
|
|
59
|
-
return join(root ?? cwd, '.cspeach');
|
|
26
|
+
return resolveProjectFolderSync(cwd).workDir;
|
|
60
27
|
}
|
|
61
28
|
/**
|
|
62
29
|
* If the workspace lives directly under a project root that has a
|
|
@@ -92,17 +59,13 @@ function maybeAddToGitignore(workspaceDir) {
|
|
|
92
59
|
}
|
|
93
60
|
}
|
|
94
61
|
/**
|
|
95
|
-
* Resolve the workspace folder PATH without creating it.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* save-command.ts, which must not leave filesystem side effects before the
|
|
100
|
-
* user says yes to the save.
|
|
62
|
+
* Resolve the workspace folder PATH without creating it. Strictly read-only:
|
|
63
|
+
* no mkdir, no .gitignore append. For callers that only need to LOOK at the
|
|
64
|
+
* workspace — e.g. the pre-confirm prior-plan scan in save-command.ts, which
|
|
65
|
+
* must not leave filesystem side effects before the user says yes to the save.
|
|
101
66
|
*/
|
|
102
67
|
export async function resolveWorkspacePath() {
|
|
103
|
-
|
|
104
|
-
const configured = cfg.workspace_folder?.trim();
|
|
105
|
-
return resolveTarget(configured);
|
|
68
|
+
return defaultWorkspaceFor(process.cwd());
|
|
106
69
|
}
|
|
107
70
|
/**
|
|
108
71
|
* App lane routing I3 — the same read-only resolution as resolveWorkspacePath,
|
|
@@ -110,64 +73,35 @@ export async function resolveWorkspacePath() {
|
|
|
110
73
|
* real session the two are the same folder; tests pass a temp cwd.
|
|
111
74
|
*/
|
|
112
75
|
export async function resolveWorkspacePathFor(cwd) {
|
|
113
|
-
|
|
114
|
-
const configured = cfg.workspace_folder?.trim();
|
|
115
|
-
return configured && configured.length > 0 ? expandHome(configured) : defaultWorkspaceFor(cwd);
|
|
116
|
-
}
|
|
117
|
-
function resolveTarget(configured) {
|
|
118
|
-
return configured && configured.length > 0
|
|
119
|
-
? expandHome(configured)
|
|
120
|
-
: defaultWorkspaceFor(process.cwd());
|
|
76
|
+
return defaultWorkspaceFor(cwd);
|
|
121
77
|
}
|
|
122
78
|
/**
|
|
123
|
-
* Resolve the workspace folder path
|
|
124
|
-
* Returns the absolute path. Idempotent.
|
|
79
|
+
* Resolve the workspace folder path for `cwd` (default: process.cwd()),
|
|
80
|
+
* creating it if missing. Returns the absolute path. Idempotent.
|
|
125
81
|
*/
|
|
126
|
-
export async function ensureWorkspace() {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// would be overreach.
|
|
137
|
-
if (!configured)
|
|
138
|
-
maybeAddToGitignore(target);
|
|
139
|
-
}
|
|
140
|
-
return target;
|
|
82
|
+
export async function ensureWorkspace(cwd = process.cwd()) {
|
|
83
|
+
const pf = resolveProjectFolderSync(cwd);
|
|
84
|
+
if (!existsSync(pf.workDir)) {
|
|
85
|
+
mkdirSync(pf.workDir, { recursive: true });
|
|
86
|
+
// Only for the folder you start in. The .gitignore beside a team folder
|
|
87
|
+
// or ~/CSPeach is not ours to edit.
|
|
88
|
+
if (pf.kind === 'solo')
|
|
89
|
+
maybeAddToGitignore(pf.workDir);
|
|
90
|
+
}
|
|
91
|
+
return pf.workDir;
|
|
141
92
|
}
|
|
142
93
|
/**
|
|
143
94
|
* Synchronous variant for callers that cannot await — most importantly
|
|
144
|
-
* Node's readline `completer` interface, which is sync.
|
|
145
|
-
* config directly via fs.readFileSync. Falls back to project-rooted
|
|
146
|
-
* default on any error (missing config, parse failure, etc.) — the
|
|
147
|
-
* completer should never throw.
|
|
148
|
-
*
|
|
149
|
-
* Earlier revision used `require('@iarna/toml')` here, which threw in
|
|
150
|
-
* ESM context (cspeach-cli has `"type": "module"`) and silently broke
|
|
151
|
-
* Tab completion. Top-level import is correct for ESM modules.
|
|
95
|
+
* Node's readline `completer` interface, which is sync. Never throws.
|
|
152
96
|
*/
|
|
153
|
-
export function ensureWorkspaceSync() {
|
|
154
|
-
|
|
97
|
+
export function ensureWorkspaceSync(cwd = process.cwd()) {
|
|
98
|
+
const target = defaultWorkspaceFor(cwd);
|
|
155
99
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const raw = readFileSync(cfgPath, 'utf-8');
|
|
159
|
-
const parsed = toml.parse(raw);
|
|
160
|
-
const configured = parsed.workspace_folder?.trim();
|
|
161
|
-
if (configured && configured.length > 0) {
|
|
162
|
-
target = expandHome(configured);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
100
|
+
if (!existsSync(target))
|
|
101
|
+
mkdirSync(target, { recursive: true });
|
|
165
102
|
}
|
|
166
103
|
catch {
|
|
167
|
-
//
|
|
168
|
-
}
|
|
169
|
-
if (!existsSync(target)) {
|
|
170
|
-
mkdirSync(target, { recursive: true });
|
|
104
|
+
// The completer must never throw.
|
|
171
105
|
}
|
|
172
106
|
return target;
|
|
173
107
|
}
|
|
@@ -206,16 +140,6 @@ export function listProjectFilesSync(workspace) {
|
|
|
206
140
|
return [];
|
|
207
141
|
}
|
|
208
142
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Expand a leading `~/` to the user's home directory. Other paths are
|
|
211
|
-
* returned unchanged.
|
|
212
|
-
*/
|
|
213
|
-
function expandHome(p) {
|
|
214
|
-
if (p.startsWith('~/') || p === '~') {
|
|
215
|
-
return join(os.homedir(), p.slice(p === '~' ? 1 : 2));
|
|
216
|
-
}
|
|
217
|
-
return p;
|
|
218
|
-
}
|
|
219
143
|
/** File suffixes recognised as text-input candidates for skills. */
|
|
220
144
|
const TEXT_SUFFIXES = ['.txt', '.md'];
|
|
221
145
|
/**
|
|
@@ -252,18 +176,63 @@ export function isAttachableFilename(name) {
|
|
|
252
176
|
* yield an `unknown` summary rather than throwing — callers see the
|
|
253
177
|
* file name with no metadata so the user can investigate.
|
|
254
178
|
*/
|
|
255
|
-
export async function listProjectFiles() {
|
|
256
|
-
const ws = await ensureWorkspace();
|
|
257
|
-
const
|
|
179
|
+
export async function listProjectFiles(cwd = process.cwd()) {
|
|
180
|
+
const ws = await ensureWorkspace(cwd);
|
|
181
|
+
const out = summarizeDir(readdirSync(ws), ws, false);
|
|
182
|
+
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
183
|
+
// 2026-10-08 (sessions/folders, Task 11) — for one release the old save
|
|
184
|
+
// locations stay readable: their files follow the new folder's, marked
|
|
185
|
+
// `earlier`. Read-only: nothing is created or written there. A file with
|
|
186
|
+
// the same name in the new folder wins.
|
|
187
|
+
const seen = new Set(out.map((f) => f.filename.toLowerCase()));
|
|
188
|
+
let legacy;
|
|
189
|
+
try {
|
|
190
|
+
legacy = legacyDirsFor(cwd);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
legacy = [];
|
|
194
|
+
}
|
|
195
|
+
for (const dir of legacy) {
|
|
196
|
+
let names;
|
|
197
|
+
try {
|
|
198
|
+
names = readdirSync(dir);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const rows = summarizeDir(names, dir, isCliHome(dir)).filter((f) => !seen.has(f.filename.toLowerCase()));
|
|
204
|
+
rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
205
|
+
for (const f of rows) {
|
|
206
|
+
seen.add(f.filename.toLowerCase());
|
|
207
|
+
out.push({ ...f, earlier: true });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Whether `dir` is the CLI home. In the CLI home only `*.cspeach.json` files
|
|
214
|
+
* directly in it are ever listed — never config.toml, auth or sessions.
|
|
215
|
+
*/
|
|
216
|
+
function isCliHome(dir) {
|
|
217
|
+
return normDir(dir) === normDir(cspeachRoot());
|
|
218
|
+
}
|
|
219
|
+
/** The supported files among `names` in `dir`, parsed for the summary. */
|
|
220
|
+
function summarizeDir(names, dir, envelopesOnly) {
|
|
258
221
|
const out = [];
|
|
259
|
-
for (const name of
|
|
222
|
+
for (const name of names) {
|
|
260
223
|
const isEnvelope = name.endsWith('.cspeach.json');
|
|
261
|
-
const isText = isTextFilename(name);
|
|
262
|
-
const isDoc = isDocFilename(name);
|
|
224
|
+
const isText = !envelopesOnly && isTextFilename(name);
|
|
225
|
+
const isDoc = !envelopesOnly && isDocFilename(name);
|
|
263
226
|
if (!isEnvelope && !isText && !isDoc)
|
|
264
227
|
continue;
|
|
265
|
-
const path = join(
|
|
266
|
-
|
|
228
|
+
const path = join(dir, name);
|
|
229
|
+
let stat;
|
|
230
|
+
try {
|
|
231
|
+
stat = statSync(path);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
267
236
|
if (!stat.isFile())
|
|
268
237
|
continue;
|
|
269
238
|
if (isEnvelope) {
|
|
@@ -307,9 +276,95 @@ export async function listProjectFiles() {
|
|
|
307
276
|
});
|
|
308
277
|
}
|
|
309
278
|
}
|
|
310
|
-
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
311
279
|
return out;
|
|
312
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* The @ completer's view of the old save locations (sync, names only): the
|
|
283
|
+
* same files listProjectFiles marks `earlier`; names in `skip` (the new
|
|
284
|
+
* folder's) are left out. Never throws.
|
|
285
|
+
*/
|
|
286
|
+
export function listLegacyFilesSync(cwd, skip) {
|
|
287
|
+
const out = [];
|
|
288
|
+
const seen = new Set([...skip].map((n) => n.toLowerCase()));
|
|
289
|
+
let dirs;
|
|
290
|
+
try {
|
|
291
|
+
dirs = legacyDirsFor(cwd);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
for (const dir of dirs) {
|
|
297
|
+
const home = isCliHome(dir);
|
|
298
|
+
for (const f of listProjectFilesSync(dir)) {
|
|
299
|
+
if (home && f.kind !== 'envelope')
|
|
300
|
+
continue;
|
|
301
|
+
if (seen.has(f.filename.toLowerCase()))
|
|
302
|
+
continue;
|
|
303
|
+
seen.add(f.filename.toLowerCase());
|
|
304
|
+
out.push(f);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
/** Number of `*.cspeach.json` files directly in `dir` (0 when unreadable). */
|
|
310
|
+
function countEnvelopes(dir) {
|
|
311
|
+
try {
|
|
312
|
+
return readdirSync(dir).filter((n) => n.endsWith('.cspeach.json')).length;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The start-up hint for files left in old save locations, or null when
|
|
320
|
+
* there are none. Names the first old folder; counts envelopes in all.
|
|
321
|
+
*/
|
|
322
|
+
export function legacyHintLine(cwd) {
|
|
323
|
+
let dirs;
|
|
324
|
+
try {
|
|
325
|
+
dirs = legacyDirsFor(cwd);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const n = dirs.reduce((sum, d) => sum + countEnvelopes(d), 0);
|
|
331
|
+
if (n === 0)
|
|
332
|
+
return null;
|
|
333
|
+
const lead = n === 1
|
|
334
|
+
? `1 project file from earlier builds is in ${dirs[0]}.`
|
|
335
|
+
: `${n} project files from earlier builds are in ${dirs[0]}.`;
|
|
336
|
+
return `${lead} Run: cspeach files sort`;
|
|
337
|
+
}
|
|
338
|
+
function localDay(d) {
|
|
339
|
+
const pad = (x) => String(x).padStart(2, '0');
|
|
340
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* At most once per folder per day (spec §3.6): true when the hint for `cwd`
|
|
344
|
+
* was not shown today, and records today in <CSPEACH_HOME>/hints.json.
|
|
345
|
+
* Best-effort: a missing or broken file counts as "not shown"; never throws.
|
|
346
|
+
*/
|
|
347
|
+
export function claimLegacyHintToday(cwd, now = new Date()) {
|
|
348
|
+
const file = join(cspeachRoot(), 'hints.json');
|
|
349
|
+
const today = localDay(now);
|
|
350
|
+
let map = {};
|
|
351
|
+
try {
|
|
352
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
353
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
354
|
+
map = parsed;
|
|
355
|
+
}
|
|
356
|
+
catch { /* missing or broken — start fresh */ }
|
|
357
|
+
const k = normDir(cwd);
|
|
358
|
+
if (map[k] === today)
|
|
359
|
+
return false;
|
|
360
|
+
map[k] = today;
|
|
361
|
+
try {
|
|
362
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
363
|
+
writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`, 'utf8');
|
|
364
|
+
}
|
|
365
|
+
catch { /* best-effort */ }
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
313
368
|
/**
|
|
314
369
|
* Inspect a `design` envelope and decide whether the design describes a
|
|
315
370
|
* RAP stack (Behavior Definition / Behavior Pool / Service Binding) or a
|
|
@@ -390,6 +445,12 @@ export function resolveAtToken(args) {
|
|
|
390
445
|
if (exact.length === 1) {
|
|
391
446
|
return { kind: 'path', path: exact[0].path };
|
|
392
447
|
}
|
|
448
|
+
// Final fix wave, item 6 — one match in this folder beats matches that are
|
|
449
|
+
// only in an earlier location.
|
|
450
|
+
const here = matches.filter((f) => !f.earlier);
|
|
451
|
+
if (here.length === 1) {
|
|
452
|
+
return { kind: 'path', path: here[0].path };
|
|
453
|
+
}
|
|
393
454
|
return { kind: 'ambiguous', matches };
|
|
394
455
|
}
|
|
395
456
|
/**
|
|
@@ -478,8 +539,8 @@ export function resolveBareTokenInCwd(token, cwd) {
|
|
|
478
539
|
* a plain `@spec.txt` instead of an absolute path.
|
|
479
540
|
*/
|
|
480
541
|
export async function resolveAtTokenAsync(token, cwd) {
|
|
481
|
-
const workspace = await ensureWorkspace();
|
|
482
|
-
const files = await listProjectFiles();
|
|
542
|
+
const workspace = await ensureWorkspace(cwd);
|
|
543
|
+
const files = await listProjectFiles(cwd);
|
|
483
544
|
const result = resolveAtToken({ token, cwd, workspace, files });
|
|
484
545
|
if (result.kind === 'notFound' && isBareToken(token)) {
|
|
485
546
|
const cwdResult = resolveBareTokenInCwd(token, cwd);
|
|
@@ -506,7 +567,7 @@ export function formatProjectFileList(files, workspace) {
|
|
|
506
567
|
const meta = f.kind === 'envelope'
|
|
507
568
|
? `${f.artefactType.padEnd(9)} v${f.version ?? '?'}`
|
|
508
569
|
: `${f.kind.padEnd(9)} ${formatBytes(f.sizeBytes).padStart(7)}`;
|
|
509
|
-
lines.push(` ${f.filename} — ${meta} ${ageStr}`);
|
|
570
|
+
lines.push(` ${f.filename} — ${meta} ${ageStr}${f.earlier ? ' (earlier location)' : ''}`);
|
|
510
571
|
}
|
|
511
572
|
lines.push('');
|
|
512
573
|
lines.push(`${files.length} file(s). Use @<partial-name> to load — e.g. @${shortHint(files[0].filename)}`);
|
|
@@ -50,6 +50,43 @@ export async function listCustomPackages(sql, prefixes = ['Z', 'Y']) {
|
|
|
50
50
|
}
|
|
51
51
|
return [...out.values()].sort((a, b) => a.package.localeCompare(b.package));
|
|
52
52
|
}
|
|
53
|
+
/** The data preview never returns more than this many rows in one answer. */
|
|
54
|
+
export const SQL_ROW_CAP = 5000;
|
|
55
|
+
/** A quote in a paging key is doubled, so it cannot end the literal. */
|
|
56
|
+
const lit = (v) => v.replace(/\x27/g, '\x27\x27');
|
|
57
|
+
/**
|
|
58
|
+
* One package (or one $TMP prefix). Below the cap: today's single query for
|
|
59
|
+
* `expected + 50` rows. Above it (`expected + 50 > 5000`, review I14): keyset
|
|
60
|
+
* pages of 5,000 by OBJ_NAME until a page is short. The key is `>=` with the
|
|
61
|
+
* rows already seen dropped, so a name held by two object types that a page
|
|
62
|
+
* boundary splits is never lost. The control total of the caller still decides.
|
|
63
|
+
*/
|
|
64
|
+
async function pullOne(sql, pkg, select, expected) {
|
|
65
|
+
if (expected + 50 <= SQL_ROW_CAP)
|
|
66
|
+
return (await sql(select, expected + 50)).rows;
|
|
67
|
+
const rows = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
let last;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const page = (await sql(`${select}${last === undefined ? '' : ` AND OBJ_NAME >= '${lit(last)}'`} ORDER BY OBJ_NAME, OBJECT`, SQL_ROW_CAP)).rows;
|
|
72
|
+
let fresh = 0;
|
|
73
|
+
for (const r of page) {
|
|
74
|
+
const k = `${clean(r.OBJECT)}|${clean(r.OBJ_NAME)}`;
|
|
75
|
+
if (seen.has(k))
|
|
76
|
+
continue;
|
|
77
|
+
seen.add(k);
|
|
78
|
+
rows.push(r);
|
|
79
|
+
fresh += 1;
|
|
80
|
+
}
|
|
81
|
+
if (page.length < SQL_ROW_CAP)
|
|
82
|
+
return rows;
|
|
83
|
+
const next = clean(page[page.length - 1].OBJ_NAME);
|
|
84
|
+
if (fresh === 0 || next === last) {
|
|
85
|
+
throw new BaselineError(`Package ${pkg}: the pull could not page past ${next}. The baseline was NOT written.`);
|
|
86
|
+
}
|
|
87
|
+
last = next;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
53
90
|
export async function pullBaseline(sql, opts) {
|
|
54
91
|
const types = new Set((opts.objectTypes ?? CODE_OBJECT_TYPES).map((t) => t.toUpperCase()));
|
|
55
92
|
const objects = [];
|
|
@@ -65,11 +102,11 @@ export async function pullBaseline(sql, opts) {
|
|
|
65
102
|
for (const [prefix, n] of Object.entries(byPrefix)) {
|
|
66
103
|
if (!PREFIX_RE.test(prefix))
|
|
67
104
|
throw new BaselineError(`Not a valid name prefix: ${prefix}`);
|
|
68
|
-
res.rows.push(...(await sql
|
|
105
|
+
res.rows.push(...(await pullOne(sql, pkg, `${select} AND OBJ_NAME LIKE '${prefix}%'`, n)));
|
|
69
106
|
}
|
|
70
107
|
}
|
|
71
108
|
else {
|
|
72
|
-
res.rows.push(...(await sql
|
|
109
|
+
res.rows.push(...(await pullOne(sql, pkg, select, expected)));
|
|
73
110
|
}
|
|
74
111
|
if (res.rows.length !== expected) {
|
|
75
112
|
throw new BaselineError(`Package ${pkg}: the system counted ${expected} objects but the pull returned ${res.rows.length}. `
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision rules for the joined team project (step 2, D6): which rule decides
|
|
3
|
+
* each object, read from the same register and findings team_findings shows,
|
|
4
|
+
* with the project's own rules or the defaults. The row cards use it to say
|
|
5
|
+
* "Rule <id>: …" or "Differs from rule <id>: …". Read only; never writes the
|
|
6
|
+
* rules to the file.
|
|
7
|
+
*/
|
|
8
|
+
import { evaluateRules, readDecisionRules } from '@cspeach/register-core';
|
|
9
|
+
import { findingsByObject } from './findings.js';
|
|
10
|
+
import { registerFor } from './picture.js';
|
|
11
|
+
import { readProjectFile } from './store.js';
|
|
12
|
+
/** The rule that decides one register object, from its latest counted run's findings. */
|
|
13
|
+
export function ruleProposalOf(o, f, rules) {
|
|
14
|
+
return evaluateRules(o, rules, f ? { findings: f.latest } : {});
|
|
15
|
+
}
|
|
16
|
+
/** "TYPE|NAME" → the deciding rule (or null), for every object of the team project; `warning` when the file's rules could not be read. */
|
|
17
|
+
export function ruleProposalsFor(projectDir, outboxDir) {
|
|
18
|
+
const rules = readDecisionRules(readProjectFile(projectDir));
|
|
19
|
+
const { register } = registerFor(projectDir, outboxDir);
|
|
20
|
+
const findings = findingsByObject(projectDir, outboxDir);
|
|
21
|
+
const byKey = new Map(register.objects.map((o) => [o.key.toUpperCase(), o]));
|
|
22
|
+
const cache = new Map();
|
|
23
|
+
return {
|
|
24
|
+
lookup: (key) => {
|
|
25
|
+
const k = key.trim().toUpperCase();
|
|
26
|
+
if (cache.has(k))
|
|
27
|
+
return cache.get(k);
|
|
28
|
+
const o = byKey.get(k);
|
|
29
|
+
const p = o ? ruleProposalOf(o, findings.get(o.key), rules.rules) : null;
|
|
30
|
+
cache.set(k, p);
|
|
31
|
+
return p;
|
|
32
|
+
},
|
|
33
|
+
...(rules.warning ? { warning: rules.warning } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|