@voxgig/sdkgen 3.4.7 → 3.5.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/bin/voxgig-sdkgen +78 -10
- package/dist/action/action.d.ts +5 -2
- package/dist/action/action.js +113 -6
- package/dist/action/action.js.map +1 -1
- package/dist/action/check.d.ts +15 -0
- package/dist/action/check.js +347 -0
- package/dist/action/check.js.map +1 -0
- package/dist/action/dispatch.d.ts +8 -0
- package/dist/action/dispatch.js +74 -0
- package/dist/action/dispatch.js.map +1 -0
- package/dist/action/docs.d.ts +4 -0
- package/dist/action/docs.js +236 -0
- package/dist/action/docs.js.map +1 -0
- package/dist/action/doctor.d.ts +5 -2
- package/dist/action/doctor.js +526 -74
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/feature.js +96 -17
- package/dist/action/feature.js.map +1 -1
- package/dist/action/kind.d.ts +30 -0
- package/dist/action/kind.js +258 -0
- package/dist/action/kind.js.map +1 -0
- package/dist/action/package.d.ts +25 -0
- package/dist/action/package.js +852 -0
- package/dist/action/package.js.map +1 -0
- package/dist/action/resolve.d.ts +18 -0
- package/dist/action/resolve.js +280 -0
- package/dist/action/resolve.js.map +1 -0
- package/dist/action/target.d.ts +7 -1
- package/dist/action/target.js +240 -97
- package/dist/action/target.js.map +1 -1
- package/dist/cmp/Docs.d.ts +4 -0
- package/dist/cmp/Docs.js +98 -0
- package/dist/cmp/Docs.js.map +1 -0
- package/dist/cmp/ExternalDocs.d.ts +2 -0
- package/dist/cmp/ExternalDocs.js +43 -0
- package/dist/cmp/ExternalDocs.js.map +1 -0
- package/dist/helpers/definition.d.ts +5 -0
- package/dist/helpers/definition.js +72 -0
- package/dist/helpers/definition.js.map +1 -0
- package/dist/helpers/featureSource.d.ts +8 -2
- package/dist/helpers/featureSource.js +61 -12
- package/dist/helpers/featureSource.js.map +1 -1
- package/dist/helpers/junk.d.ts +11 -0
- package/dist/helpers/junk.js +149 -0
- package/dist/helpers/junk.js.map +1 -0
- package/dist/helpers/manifest.d.ts +50 -0
- package/dist/helpers/manifest.js +328 -0
- package/dist/helpers/manifest.js.map +1 -0
- package/dist/helpers/modelcheck.d.ts +22 -0
- package/dist/helpers/modelcheck.js +203 -0
- package/dist/helpers/modelcheck.js.map +1 -0
- package/dist/helpers/semver.d.ts +4 -0
- package/dist/helpers/semver.js +174 -0
- package/dist/helpers/semver.js.map +1 -0
- package/dist/helpers/shipped.d.ts +4 -0
- package/dist/helpers/shipped.js +36 -0
- package/dist/helpers/shipped.js.map +1 -0
- package/dist/helpers/stdrep.d.ts +9 -1
- package/dist/helpers/stdrep.js +22 -0
- package/dist/helpers/stdrep.js.map +1 -1
- package/dist/sdkgen.d.ts +15 -3
- package/dist/sdkgen.js +233 -67
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/utility.js +17 -0
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aontu +69 -0
- package/package.json +3 -2
- package/project/.sdk/model/feature/audit.aontu +1 -0
- package/project/.sdk/model/feature/cache.aontu +1 -0
- package/project/.sdk/model/feature/clienttrack.aontu +1 -0
- package/project/.sdk/model/feature/debug.aontu +1 -0
- package/project/.sdk/model/feature/idempotency.aontu +1 -0
- package/project/.sdk/model/feature/log.aontu +1 -0
- package/project/.sdk/model/feature/metrics.aontu +1 -0
- package/project/.sdk/model/feature/netsim.aontu +1 -0
- package/project/.sdk/model/feature/paging.aontu +1 -0
- package/project/.sdk/model/feature/proxy.aontu +1 -0
- package/project/.sdk/model/feature/ratelimit.aontu +1 -0
- package/project/.sdk/model/feature/rbac.aontu +1 -0
- package/project/.sdk/model/feature/retry.aontu +1 -0
- package/project/.sdk/model/feature/streaming.aontu +1 -0
- package/project/.sdk/model/feature/telemetry.aontu +1 -0
- package/project/.sdk/model/feature/test.aontu +1 -0
- package/project/.sdk/model/feature/timeout.aontu +1 -0
- package/project/.sdk/model/target/c.aontu +1 -0
- package/project/.sdk/model/target/clojure.aontu +1 -0
- package/project/.sdk/model/target/cpp.aontu +1 -0
- package/project/.sdk/model/target/dart.aontu +1 -0
- package/project/.sdk/model/target/elixir.aontu +1 -0
- package/project/.sdk/model/target/go-cli.aontu +1 -0
- package/project/.sdk/model/target/go-mcp.aontu +1 -0
- package/project/.sdk/model/target/go.aontu +1 -0
- package/project/.sdk/model/target/haskell.aontu +1 -0
- package/project/.sdk/model/target/java.aontu +1 -0
- package/project/.sdk/model/target/js.aontu +1 -0
- package/project/.sdk/model/target/kotlin.aontu +1 -0
- package/project/.sdk/model/target/lean.aontu +1 -0
- package/project/.sdk/model/target/lua.aontu +1 -0
- package/project/.sdk/model/target/ocaml.aontu +1 -0
- package/project/.sdk/model/target/perl.aontu +1 -0
- package/project/.sdk/model/target/php.aontu +1 -0
- package/project/.sdk/model/target/py-data.aontu +1 -0
- package/project/.sdk/model/target/py.aontu +1 -0
- package/project/.sdk/model/target/rb.aontu +1 -0
- package/project/.sdk/model/target/rust.aontu +1 -0
- package/project/.sdk/model/target/scala.aontu +1 -0
- package/project/.sdk/model/target/seneca-provider.aontu +1 -0
- package/project/.sdk/model/target/zig.aontu +1 -0
- package/project/.sdk/src/cmp/c/Config_c.ts +44 -0
- package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +3 -1
- package/project/.sdk/src/cmp/clojure/Config_clojure.ts +37 -42
- package/project/.sdk/src/cmp/clojure/utility_clojure.ts +42 -0
- package/project/.sdk/src/cmp/cpp/Config_cpp.ts +18 -0
- package/project/.sdk/src/cmp/csharp/Config_csharp.ts +125 -52
- package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +3 -1
- package/project/.sdk/src/cmp/csharp/utility_csharp.ts +94 -6
- package/project/.sdk/src/cmp/dart/Config_dart.ts +43 -5
- package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +74 -0
- package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -9
- package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +16 -5
- package/project/.sdk/src/cmp/dart/utility_dart.ts +18 -0
- package/project/.sdk/src/cmp/elixir/Config_elixir.ts +128 -13
- package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +3 -1
- package/project/.sdk/src/cmp/elixir/utility_elixir.ts +6 -0
- package/project/.sdk/src/cmp/haskell/Config_haskell.ts +35 -52
- package/project/.sdk/src/cmp/haskell/Package_haskell.ts +1 -0
- package/project/.sdk/src/cmp/haskell/Test_haskell.ts +1 -1
- package/project/.sdk/src/cmp/haskell/utility_haskell.ts +15 -5
- package/project/.sdk/src/cmp/java/Config_java.ts +21 -0
- package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +15 -0
- package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +42 -39
- package/project/.sdk/src/cmp/perl/Config_perl.ts +15 -0
- package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +3 -1
- package/project/.sdk/src/cmp/rust/Config_rust.ts +44 -0
- package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +4 -2
- package/project/.sdk/src/cmp/scala/Config_scala.ts +15 -0
- package/project/.sdk/src/cmp/swift/Config_swift.ts +17 -0
- package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +3 -1
- package/project/.sdk/src/cmp/zig/Config_zig.ts +44 -0
- package/project/.sdk/src/cmp/zig/TestEntity_zig.ts +9 -2
- package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +3 -1
- package/project/.sdk/tm/c/core/sdk.h +1 -0
- package/project/.sdk/tm/c/utility/make_options.c +5 -1
- package/project/.sdk/tm/clojure/src/sdk/core.clj +51 -3
- package/project/.sdk/tm/cpp/core/types.hpp +4 -1
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +5 -1
- package/project/.sdk/tm/csharp/test/PipelineTest.cs +15 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +67 -1
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +43 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +58 -2
- package/project/.sdk/tm/haskell/{test/TestJson.hs → src/SdkJson.hs} +8 -4
- package/project/.sdk/tm/haskell/src/SdkRuntime.hs +55 -2
- package/project/.sdk/tm/haskell/test/Runner.hs +1 -1
- package/project/.sdk/tm/haskell/test/StructCorpus.hs +1 -1
- package/project/.sdk/tm/java/core/SdkClient.java +3 -1
- package/project/.sdk/tm/java/utility/MakeOptions.java +5 -1
- package/project/.sdk/tm/kotlin/core/SdkClient.kt +3 -1
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +5 -1
- package/project/.sdk/tm/ocaml/Makefile +3 -2
- package/project/.sdk/tm/ocaml/sdk_json.ml +106 -0
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +56 -0
- package/project/.sdk/tm/ocaml/test/struct_corpus.ml +4 -86
- package/project/.sdk/tm/perl/utility/make_options.pm +5 -1
- package/project/.sdk/tm/rust/utility/make_options.rs +79 -1
- package/project/.sdk/tm/scala/core/SdkClient.scala +3 -1
- package/project/.sdk/tm/scala/utility/Make.scala +5 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +5 -1
- package/project/.sdk/tm/zig/core/utility.zig +85 -1
- package/project/sdkgen-package.json +57 -0
- package/src/action/action.ts +136 -6
- package/src/action/check.ts +495 -0
- package/src/action/dispatch.ts +105 -0
- package/src/action/docs.ts +310 -0
- package/src/action/doctor.ts +692 -81
- package/src/action/feature.ts +109 -17
- package/src/action/kind.ts +375 -0
- package/src/action/package.ts +1173 -0
- package/src/action/resolve.ts +362 -0
- package/src/action/target.ts +290 -108
- package/src/cmp/Docs.ts +125 -0
- package/src/cmp/ExternalDocs.ts +54 -0
- package/src/helpers/definition.ts +82 -0
- package/src/helpers/featureSource.ts +83 -13
- package/src/helpers/junk.ts +168 -0
- package/src/helpers/manifest.ts +474 -0
- package/src/helpers/modelcheck.ts +262 -0
- package/src/helpers/semver.ts +211 -0
- package/src/helpers/shipped.ts +41 -0
- package/src/helpers/stdrep.ts +70 -0
- package/src/sdkgen.ts +294 -66
- package/src/types.ts +33 -3
- package/src/utility.ts +17 -0
package/src/action/doctor.ts
CHANGED
|
@@ -44,18 +44,43 @@ import type {
|
|
|
44
44
|
|
|
45
45
|
import { SdkGenError } from '../utility'
|
|
46
46
|
|
|
47
|
-
import { templateReplacements } from '../helpers/stdrep'
|
|
47
|
+
import { templateReplacements, provenanceReplace } from '../helpers/stdrep'
|
|
48
48
|
|
|
49
49
|
import {
|
|
50
|
-
resolveTarget,
|
|
51
50
|
trimFeatures,
|
|
51
|
+
aliasCmpText,
|
|
52
|
+
aliasCmpName,
|
|
52
53
|
} from './target'
|
|
53
54
|
|
|
55
|
+
// `aliasModelText` is no longer named here: the model-file comparison asks
|
|
56
|
+
// the registry for the kind's own `rename`, so a kind that renames
|
|
57
|
+
// differently is handled without this file knowing how.
|
|
58
|
+
import { recordedRef, KINDS, kindDef, kindTrees } from './kind'
|
|
59
|
+
import type { TreeDef } from './kind'
|
|
60
|
+
|
|
61
|
+
import { resolveSource } from './resolve'
|
|
62
|
+
import type { Source } from './resolve'
|
|
63
|
+
|
|
64
|
+
import { definitionPath } from '../helpers/definition'
|
|
65
|
+
|
|
66
|
+
import { findFeatureSources } from '../helpers/featureSource'
|
|
67
|
+
|
|
68
|
+
import { isJunk } from '../helpers/junk'
|
|
69
|
+
|
|
54
70
|
|
|
55
71
|
// jostraca's Copy walk skips these (IGNORED_RE in CopyOp) — editor backups and
|
|
56
72
|
// deliberately-disabled templates never reach a project, so they are not drift.
|
|
73
|
+
//
|
|
74
|
+
// `isJunk` is the other half of the same statement: what the WRITER refuses to
|
|
75
|
+
// copy, this reader must refuse to miss. A `__pycache__` in the scaffold would
|
|
76
|
+
// otherwise be reported as missing from every project on earth, and one in a
|
|
77
|
+
// project as an unexplained extra file. See helpers/junk.
|
|
57
78
|
const IGNORED_RE = /(~|-jostraca-off)$/
|
|
58
79
|
|
|
80
|
+
function ignoredEntry(name: string): boolean {
|
|
81
|
+
return IGNORED_RE.test(name) || isJunk(name)
|
|
82
|
+
}
|
|
83
|
+
|
|
59
84
|
// Extensions jostraca copies byte-for-byte. Comparing them as text would
|
|
60
85
|
// report spurious differences, so they are compared by raw bytes.
|
|
61
86
|
const BINARY_RE = /\.(png|jpg|jpeg|gif|ico|pdf|zip|gz|woff2?|ttf|eot|wasm)$/i
|
|
@@ -107,6 +132,18 @@ type DoctorReport = {
|
|
|
107
132
|
// wiring never calls. Informational: opting out is legitimate.
|
|
108
133
|
unwired: string[]
|
|
109
134
|
|
|
135
|
+
// A copy that differs from the scaffold ONLY in its provenance lines —
|
|
136
|
+
// written before `base`/`origname` were stamped, so the project changed
|
|
137
|
+
// nothing and `target add` will bring it up to date. Informational: this
|
|
138
|
+
// exists so the provenance rollout does not read as a fork in every
|
|
139
|
+
// project at once.
|
|
140
|
+
resyncPending: string[]
|
|
141
|
+
|
|
142
|
+
// An ALIASED target's model file differs from what its origin would
|
|
143
|
+
// produce. Informational: that file is project-owned, and differentiating
|
|
144
|
+
// it is what an alias is FOR.
|
|
145
|
+
aliasedDiff: string[]
|
|
146
|
+
|
|
110
147
|
// True when nothing in the first four categories was found.
|
|
111
148
|
ok: boolean
|
|
112
149
|
}
|
|
@@ -136,9 +173,19 @@ async function cmd_doctor_check(_args: string[], actx: ActionContext): Promise<A
|
|
|
136
173
|
}
|
|
137
174
|
|
|
138
175
|
|
|
176
|
+
// Narrows the check to particular items — `package update` runs doctor as
|
|
177
|
+
// its pre-check and needs the verdict for ONE package's items, not the
|
|
178
|
+
// project's. Reusing the real comparison rather than writing a second one is
|
|
179
|
+
// the point: a gate that decides whether to overwrite a project's files must
|
|
180
|
+
// not have its own idea of what counts as a difference.
|
|
181
|
+
type DoctorScope = (kind: string, name: string) => boolean
|
|
182
|
+
|
|
183
|
+
|
|
139
184
|
// Code API. Returns the report; the CLI turns a non-ok report into a
|
|
140
185
|
// non-zero exit so this can gate CI.
|
|
141
|
-
async function doctor(
|
|
186
|
+
async function doctor(
|
|
187
|
+
actx: ActionContext, scope?: DoctorScope,
|
|
188
|
+
): Promise<ActionResult> {
|
|
142
189
|
const log = actx.log
|
|
143
190
|
const fs = actx.fs()
|
|
144
191
|
const model = actx.model
|
|
@@ -146,39 +193,94 @@ async function doctor(actx: ActionContext): Promise<ActionResult> {
|
|
|
146
193
|
|
|
147
194
|
const report: DoctorReport = {
|
|
148
195
|
forked: [], edited: [], stale: [], missing: [], additive: [],
|
|
149
|
-
unwired: [], ok: true,
|
|
196
|
+
unwired: [], resyncPending: [], aliasedDiff: [], ok: true,
|
|
150
197
|
}
|
|
151
198
|
|
|
152
|
-
|
|
199
|
+
// EVERY KIND, not just targets.
|
|
200
|
+
//
|
|
201
|
+
// `add` writes a copied model file for each kind — `model/target/<t>.aontu`
|
|
202
|
+
// and `model/feature/<f>.aontu` alike — and overwrites it on every resync.
|
|
203
|
+
// Only the target one was ever compared, so a hand-edit to an installed
|
|
204
|
+
// FEATURE definition read as perfectly in sync and was silently reverted by
|
|
205
|
+
// the next `target add` (which re-runs `feature add` for every active
|
|
206
|
+
// feature). That is exactly the failure `checkTargetModel` was added for,
|
|
207
|
+
// left open for the other half of what add owns.
|
|
208
|
+
//
|
|
209
|
+
// Driven off the registry, so a kind added later is checked without
|
|
210
|
+
// anything here changing. The per-kind EXTRAS — a target's `src/cmp` and
|
|
211
|
+
// `tm` trees — stay in that kind's own check, because they are genuinely
|
|
212
|
+
// different work rather than the same work with different strings.
|
|
213
|
+
const kinds = Object.keys(KINDS).sort()
|
|
214
|
+
|
|
215
|
+
const counts: Record<string, number> = {}
|
|
216
|
+
for (const kind of kinds) {
|
|
217
|
+
counts[kind] = Object.keys((model as any)?.main?.[KIT]?.[kind] ?? {}).length
|
|
218
|
+
}
|
|
153
219
|
|
|
154
|
-
log.info({ point: 'doctor-start', targets:
|
|
220
|
+
log.info({ point: 'doctor-start', targets: counts.target ?? 0, ...counts })
|
|
155
221
|
|
|
156
|
-
|
|
157
|
-
|
|
222
|
+
// Targets resolved ONCE, before the kind loop, because the feature checks
|
|
223
|
+
// need them too: a feature's per-target source lives under the name the
|
|
224
|
+
// target has in its own source, so finding it means knowing where each
|
|
225
|
+
// target came from. Resolving per (feature, target) pair would re-read
|
|
226
|
+
// every package manifest dozens of times and warn repeatedly about the
|
|
227
|
+
// same unresolvable target.
|
|
228
|
+
//
|
|
229
|
+
// Resolved even when the run is SCOPED past them — a scoped check of one
|
|
230
|
+
// feature still has to look in every target's tree for that feature's
|
|
231
|
+
// files.
|
|
232
|
+
const targets = new Map<string, Source>()
|
|
158
233
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
Path.join(declared.base, '..', tname) : tname
|
|
234
|
+
for (const tname of
|
|
235
|
+
Object.keys((model as any)?.main?.[KIT]?.target ?? {}).sort()) {
|
|
236
|
+
const source = resolveDeclared('target', tname, actx)
|
|
163
237
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
resolved = resolveTarget(tref, { folder: root, fs: () => fs })
|
|
167
|
-
}
|
|
168
|
-
catch (err: any) {
|
|
169
|
-
log.warn({
|
|
170
|
-
point: 'doctor-target-unresolved', target: tname, err: err.message,
|
|
171
|
-
note: tname + ': cannot find its scaffold (' + err.message + ')'
|
|
172
|
-
})
|
|
173
|
-
continue
|
|
238
|
+
if (null != source) {
|
|
239
|
+
targets.set(tname, source)
|
|
174
240
|
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const kind of kinds) {
|
|
244
|
+
const items = Object.keys((model as any)?.main?.[KIT]?.[kind] ?? {}).sort()
|
|
175
245
|
|
|
176
|
-
|
|
177
|
-
|
|
246
|
+
for (const name of items) {
|
|
247
|
+
if (null != scope && !scope(kind, name)) {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const source = 'target' === kind ?
|
|
252
|
+
targets.get(name) : resolveDeclared(kind, name, actx)
|
|
253
|
+
|
|
254
|
+
if (null == source) {
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if ('target' === kind) {
|
|
259
|
+
checkTarget(actx, source, report)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if ('docs' === kind) {
|
|
263
|
+
checkDocs(actx, source, report)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Only an ACTIVE feature has source copied out; what an inactive one
|
|
267
|
+
// left behind is stale, and the target walk reports it as such.
|
|
268
|
+
if ('feature' === kind &&
|
|
269
|
+
false !== (model as any)?.main?.[KIT]?.feature?.[name]?.active) {
|
|
270
|
+
checkFeatureSource(actx, source, targets, report)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
checkItemModel(actx, kind, source, report)
|
|
178
274
|
}
|
|
179
275
|
}
|
|
180
276
|
|
|
181
|
-
|
|
277
|
+
// Root-component wiring is a property of the PROJECT, not of any item, so a
|
|
278
|
+
// scoped run has no business reporting on it — `package update` asking "is
|
|
279
|
+
// this package's copy clean" should not also say the project never wired in
|
|
280
|
+
// `ReadmeTop`.
|
|
281
|
+
if (null == scope) {
|
|
282
|
+
checkWiring(actx, report)
|
|
283
|
+
}
|
|
182
284
|
|
|
183
285
|
report.ok = 0 === report.forked.length + report.edited.length +
|
|
184
286
|
report.stale.length + report.missing.length
|
|
@@ -190,6 +292,8 @@ async function doctor(actx: ActionContext): Promise<ActionResult> {
|
|
|
190
292
|
['missing', 'MISSING (would be written by `target add`)'],
|
|
191
293
|
['additive', 'additive (project-owned, not drift)'],
|
|
192
294
|
['unwired', 'NOT WIRED IN (root capability this project is missing)'],
|
|
295
|
+
['resyncPending', 'RESYNC PENDING (predates provenance; `target add` updates it)'],
|
|
296
|
+
['aliasedDiff', 'aliased model differs from its origin (project-owned, not drift)'],
|
|
193
297
|
] as [keyof DoctorReport, string][]) {
|
|
194
298
|
for (const file of (report[kind] as string[])) {
|
|
195
299
|
log.info({ point: 'doctor-finding', kind, file, note: note + ': ' + file })
|
|
@@ -205,6 +309,8 @@ async function doctor(actx: ActionContext): Promise<ActionResult> {
|
|
|
205
309
|
missing: report.missing.length,
|
|
206
310
|
additive: report.additive.length,
|
|
207
311
|
unwired: report.unwired.length,
|
|
312
|
+
resyncPending: report.resyncPending.length,
|
|
313
|
+
aliasedDiff: report.aliasedDiff.length,
|
|
208
314
|
note: report.ok ?
|
|
209
315
|
('.sdk matches the scaffold (' + report.additive.length + ' additive)') :
|
|
210
316
|
('.sdk has drifted: ' + report.forked.length + ' forked, ' +
|
|
@@ -212,7 +318,7 @@ async function doctor(actx: ActionContext): Promise<ActionResult> {
|
|
|
212
318
|
report.missing.length + ' missing')
|
|
213
319
|
})
|
|
214
320
|
|
|
215
|
-
return { report }
|
|
321
|
+
return { report }
|
|
216
322
|
}
|
|
217
323
|
|
|
218
324
|
|
|
@@ -247,8 +353,53 @@ function checkWiring(actx: ActionContext, report: DoctorReport) {
|
|
|
247
353
|
}
|
|
248
354
|
|
|
249
355
|
|
|
250
|
-
|
|
251
|
-
|
|
356
|
+
// Where one declared item came from, or undefined with the reason said out
|
|
357
|
+
// loud.
|
|
358
|
+
//
|
|
359
|
+
// The model records `base` and `origname` (written by add), and `origname` is
|
|
360
|
+
// what makes an ALIAS checkable: the ref used to be rebuilt as
|
|
361
|
+
// `<base>/../<name>`, which names the INSTALLED item — so `target add ts~ts2`
|
|
362
|
+
// sent doctor looking for a `ts2` scaffold that does not exist, both trees
|
|
363
|
+
// walked empty, and every component read as `additive` while every template
|
|
364
|
+
// read as `stale` (a FAILING category). Real edits were undetectable and
|
|
365
|
+
// doctor went red on noise.
|
|
366
|
+
//
|
|
367
|
+
// A bare name falls back to the bundled scaffold, for a copy predating
|
|
368
|
+
// provenance.
|
|
369
|
+
function resolveDeclared(
|
|
370
|
+
kind: string, name: string, actx: ActionContext,
|
|
371
|
+
): Source | undefined {
|
|
372
|
+
const declared: any = (actx.model as any)?.main?.[KIT]?.[kind]?.[name]
|
|
373
|
+
const ref = recordedRef(declared, name) || name
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
// WITH THE LOG. Resolution reads the source's package manifest, and an
|
|
377
|
+
// unusable one is reported only through `ctx$.log`. Without it doctor
|
|
378
|
+
// silently lost the package name — and then reported every item from that
|
|
379
|
+
// package as FORKED, because the expected text no longer carries the
|
|
380
|
+
// `package:` line the copy has, with nothing anywhere saying why. That is
|
|
381
|
+
// accurate (the next add really would strip the line) but unactionable,
|
|
382
|
+
// which is the same defect as being wrong.
|
|
383
|
+
return resolveSource(
|
|
384
|
+
ref, kind, { folder: actx.folder, fs: actx.fs, log: actx.log })
|
|
385
|
+
}
|
|
386
|
+
catch (err: any) {
|
|
387
|
+
actx.log.warn({
|
|
388
|
+
point: 'doctor-source-unresolved', kind, [kind]: name, err: err.message,
|
|
389
|
+
note: name + ': cannot find its ' + kind + ' source (' +
|
|
390
|
+
err.message + ')'
|
|
391
|
+
})
|
|
392
|
+
return undefined
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
function checkTarget(
|
|
398
|
+
actx: ActionContext, resolved: Source, report: DoctorReport,
|
|
399
|
+
) {
|
|
400
|
+
const tname = resolved.name
|
|
401
|
+
const tfolder = resolved.folder
|
|
402
|
+
const torigname = resolved.origname
|
|
252
403
|
const fs = actx.fs()
|
|
253
404
|
const model = actx.model
|
|
254
405
|
const root = actx.folder
|
|
@@ -258,14 +409,29 @@ function checkTarget(actx: ActionContext, resolved: any, report: DoctorReport) {
|
|
|
258
409
|
//
|
|
259
410
|
// src/cmp — copied verbatim, so a byte compare is the truth.
|
|
260
411
|
// tm — copied through jostraca's template(), with ProjectName.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
412
|
+
// An ALIASED install renames every `<Cmp>_<origname>` component to
|
|
413
|
+
// `<Cmp>_<alias>` and rewrites the origin name inside the file (sibling
|
|
414
|
+
// imports, the __dirname-relative fragment path), because components are
|
|
415
|
+
// dispatched by the convention `cmp/<t>/Main_<t>`. doctor has to apply the
|
|
416
|
+
// same two transforms or it expects the ORIGIN names in the alias's folder
|
|
417
|
+
// and reports the entire tree as missing — 25 files for `go~go2`, none of
|
|
418
|
+
// them a real finding.
|
|
419
|
+
const aliased = tname !== torigname
|
|
420
|
+
|
|
421
|
+
const renameCmp = aliased ?
|
|
422
|
+
(rel: string) => aliasCmpName(rel, torigname, tname) : undefined
|
|
423
|
+
|
|
424
|
+
const rewriteCmp = aliased ?
|
|
425
|
+
(src: string) => aliasCmpText(src, torigname, tname) : undefined
|
|
426
|
+
|
|
427
|
+
const trees: TreeCompare[] = [
|
|
264
428
|
{
|
|
265
429
|
project: Path.join(root, 'src', 'cmp', tname),
|
|
266
430
|
scaffold: Path.join(tfolder, 'src', 'cmp', torigname),
|
|
267
431
|
replace: {},
|
|
268
432
|
kind: 'forked',
|
|
433
|
+
rename: renameCmp,
|
|
434
|
+
rewrite: rewriteCmp,
|
|
269
435
|
},
|
|
270
436
|
{
|
|
271
437
|
project: Path.join(root, 'tm', tname),
|
|
@@ -285,18 +451,91 @@ function checkTarget(actx: ActionContext, resolved: any, report: DoctorReport) {
|
|
|
285
451
|
...Object.keys(featuremodel).filter((n: string) => false !== featuremodel[n]?.active),
|
|
286
452
|
]))
|
|
287
453
|
|
|
454
|
+
// `folder` and `model` matter: the trim catalogue is resolved consumer-side
|
|
455
|
+
// (see featureCatalogue), so a doctor that withheld them would compute a
|
|
456
|
+
// different trim from the one `target add` applied and report correctly
|
|
457
|
+
// trimmed files as missing.
|
|
288
458
|
const excludes: RegExp[] = trimFeatures(
|
|
289
|
-
{ log: quietLog(actx.log), fs: () => fs
|
|
459
|
+
{ log: quietLog(actx.log), fs: () => fs, folder: root, model },
|
|
460
|
+
tfolder, torigname, tname, features)
|
|
461
|
+
|
|
462
|
+
compareTrees(actx, report, trees, {
|
|
463
|
+
excludes,
|
|
464
|
+
// Only a TARGET has foreign feature source landing in its tree.
|
|
465
|
+
foreign: (kind: string) => 'edited' === kind ?
|
|
466
|
+
foreignFeatureSource(actx, resolved) : [],
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
// COMPARE A SET OF TREES against the sources they were copied from.
|
|
472
|
+
//
|
|
473
|
+
// Extracted from `checkTarget` unchanged, because the docs kind needs exactly
|
|
474
|
+
// this and a second copy of it is the defect this workstream keeps producing.
|
|
475
|
+
// What differs per kind stays in the caller: which trees, what the copy
|
|
476
|
+
// substituted, whether a trim excludes part of the source, and whether
|
|
477
|
+
// anything foreign is expected to land in the tree.
|
|
478
|
+
//
|
|
479
|
+
// This is the mechanism behind the rule in CLAUDE.md — anything an add
|
|
480
|
+
// writes, doctor must compare, or the next add silently reverts a project's
|
|
481
|
+
// edit. A kind that declares trees and is not walked here would break it.
|
|
482
|
+
function compareTrees(
|
|
483
|
+
actx: ActionContext,
|
|
484
|
+
report: DoctorReport,
|
|
485
|
+
trees: TreeCompare[],
|
|
486
|
+
opts?: {
|
|
487
|
+
excludes?: RegExp[],
|
|
488
|
+
foreign?: (kind: string) => Iterable<[string, string]>,
|
|
489
|
+
},
|
|
490
|
+
) {
|
|
491
|
+
const fs = actx.fs()
|
|
492
|
+
const model = actx.model
|
|
493
|
+
const root = actx.folder
|
|
494
|
+
const excludes = opts?.excludes ?? []
|
|
290
495
|
|
|
291
496
|
for (const tree of trees) {
|
|
292
497
|
// Findings are reported at project-relative paths, the way a maintainer
|
|
293
498
|
// would type them.
|
|
294
499
|
const label = Path.relative(root, tree.project).split(Path.sep).join('/') + '/'
|
|
295
500
|
|
|
296
|
-
const
|
|
501
|
+
const scaffoldFiles = 'edited' === tree.kind ?
|
|
297
502
|
walk(fs, tree.scaffold).filter((rel) => !excluded(rel, excludes)) :
|
|
298
503
|
walk(fs, tree.scaffold)
|
|
299
504
|
|
|
505
|
+
// Where each file the project should have COMES FROM: the name it lands
|
|
506
|
+
// under, mapped to the absolute path of the file it was copied from.
|
|
507
|
+
//
|
|
508
|
+
// An absolute path rather than a tree-relative one, because not every
|
|
509
|
+
// expected file comes from THIS tree — see the foreign-feature union
|
|
510
|
+
// below.
|
|
511
|
+
const landed = new Map<string, string>(scaffoldFiles.map(
|
|
512
|
+
(rel: string) => [
|
|
513
|
+
null == tree.rename ? rel : tree.rename(rel),
|
|
514
|
+
Path.join(tree.scaffold, rel),
|
|
515
|
+
]))
|
|
516
|
+
|
|
517
|
+
// FOREIGN FEATURE SOURCE.
|
|
518
|
+
//
|
|
519
|
+
// A feature supplied by a different package than the target ships its
|
|
520
|
+
// per-target source in ITS OWN `tm/<target>/` overlay, and `feature add`
|
|
521
|
+
// copies that into the project. Compared against the target's scaffold
|
|
522
|
+
// alone, those files are present in the project and absent upstream — so
|
|
523
|
+
// every one of them was reported STALE, a FAILING category. Any project
|
|
524
|
+
// using an external feature had a red `doctor` and nothing wrong with it.
|
|
525
|
+
//
|
|
526
|
+
// They are not stale, they are expected — just expected from somewhere
|
|
527
|
+
// else. Adding them here rather than suppressing them keeps them
|
|
528
|
+
// CHECKED: a hand-edit to a foreign feature's source is still reported,
|
|
529
|
+
// which is what `package update`'s gate needs in order to cover
|
|
530
|
+
// everything the re-add writes.
|
|
531
|
+
const foreign = new Set<string>()
|
|
532
|
+
|
|
533
|
+
for (const [rel, from] of (opts?.foreign?.(tree.kind) ?? [])) {
|
|
534
|
+
landed.set(rel, from)
|
|
535
|
+
foreign.add(rel)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const expected = Array.from(landed.keys()).sort()
|
|
300
539
|
const actual = walk(fs, tree.project)
|
|
301
540
|
|
|
302
541
|
const expectedSet = new Set(expected)
|
|
@@ -308,8 +547,19 @@ function checkTarget(actx: ActionContext, resolved: any, report: DoctorReport) {
|
|
|
308
547
|
continue
|
|
309
548
|
}
|
|
310
549
|
|
|
311
|
-
|
|
312
|
-
|
|
550
|
+
// A foreign feature's file is EXPECTED here (so it is not stale) but
|
|
551
|
+
// compared by `checkFeatureSource`, from the feature's side. Comparing
|
|
552
|
+
// it here too would report it twice on a full run — and, worse, would
|
|
553
|
+
// leave it uncompared on a run scoped to the feature alone, which is
|
|
554
|
+
// exactly when `feature add` is about to rewrite it.
|
|
555
|
+
if (foreign.has(rel)) {
|
|
556
|
+
continue
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const from = landed.get(rel) as string
|
|
560
|
+
|
|
561
|
+
if (differs(fs, from, Path.join(tree.project, rel),
|
|
562
|
+
model, tree.replace, undefined, tree.rewrite)) {
|
|
313
563
|
report[tree.kind].push(label + rel)
|
|
314
564
|
}
|
|
315
565
|
}
|
|
@@ -322,7 +572,8 @@ function checkTarget(actx: ActionContext, resolved: any, report: DoctorReport) {
|
|
|
322
572
|
// A component the scaffold has NEVER shipped is the project's own —
|
|
323
573
|
// the supported way to add a per-target component. Anything else under
|
|
324
574
|
// a tree `target add` owns is stale output.
|
|
325
|
-
const known = fs.existsSync(Path.join(tree.scaffold, rel))
|
|
575
|
+
const known = fs.existsSync(Path.join(tree.scaffold, rel)) ||
|
|
576
|
+
landed.has(rel)
|
|
326
577
|
if ('forked' === tree.kind && !known) {
|
|
327
578
|
report.additive.push(label + rel)
|
|
328
579
|
}
|
|
@@ -331,60 +582,387 @@ function checkTarget(actx: ActionContext, resolved: any, report: DoctorReport) {
|
|
|
331
582
|
}
|
|
332
583
|
}
|
|
333
584
|
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
// A DOCS item's trees.
|
|
589
|
+
//
|
|
590
|
+
// Simpler than a target's in every way that matters here: no trim (a docs
|
|
591
|
+
// item has no per-feature source to leave out), and nothing foreign lands in
|
|
592
|
+
// them (a feature package's overlay targets a TARGET's tree). What is the
|
|
593
|
+
// same is the alias handling — docs components are dispatched by the same
|
|
594
|
+
// `Main_<n>` convention — and the tree paths, which come from the registry
|
|
595
|
+
// rather than being spelled a second time.
|
|
596
|
+
//
|
|
597
|
+
// The optional template tree is skipped when the SOURCE does not ship one:
|
|
598
|
+
// `docs add` did not copy it, so the project is right not to have it.
|
|
599
|
+
function checkDocs(
|
|
600
|
+
actx: ActionContext, resolved: Source, report: DoctorReport,
|
|
601
|
+
) {
|
|
602
|
+
const fs = actx.fs()
|
|
603
|
+
const root = actx.folder
|
|
604
|
+
const name = resolved.name
|
|
605
|
+
const origname = resolved.origname
|
|
606
|
+
const aliased = name !== origname
|
|
607
|
+
|
|
608
|
+
const dest = kindTrees('docs', name)
|
|
609
|
+
const from = kindTrees('docs', origname)
|
|
334
610
|
|
|
335
|
-
|
|
611
|
+
const trees: TreeCompare[] = dest.flatMap((tree: TreeDef, i: number) => {
|
|
612
|
+
const scaffold = Path.join(resolved.folder, ...from[i].path.split('/'))
|
|
613
|
+
|
|
614
|
+
if (!fs.existsSync(scaffold)) {
|
|
615
|
+
return []
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const templated = 'template' === tree.replace
|
|
619
|
+
|
|
620
|
+
return [{
|
|
621
|
+
project: Path.join(root, ...tree.path.split('/')),
|
|
622
|
+
scaffold,
|
|
623
|
+
replace: templated ? templateReplacements(actx.model, name) : {},
|
|
624
|
+
kind: templated ? 'edited' : 'forked',
|
|
625
|
+
rename: aliased && !templated ?
|
|
626
|
+
(rel: string) => aliasCmpName(rel, origname, name) : undefined,
|
|
627
|
+
rewrite: aliased && !templated ?
|
|
628
|
+
(src: string) => aliasCmpText(src, origname, name) : undefined,
|
|
629
|
+
} as TreeCompare]
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
compareTrees(actx, report, trees)
|
|
336
633
|
}
|
|
337
634
|
|
|
338
635
|
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
636
|
+
// One tree to compare: where it landed, where it came from, what the copy
|
|
637
|
+
// substituted, and how a finding about it is categorised.
|
|
638
|
+
//
|
|
639
|
+
// forked — a verbatim copy, so any difference is a fork of the source
|
|
640
|
+
// edited — a templated copy, so a difference is an edit to the master
|
|
641
|
+
type TreeCompare = {
|
|
642
|
+
project: string
|
|
643
|
+
scaffold: string
|
|
644
|
+
replace: any
|
|
645
|
+
kind: 'forked' | 'edited'
|
|
646
|
+
rename?: (rel: string) => string
|
|
647
|
+
rewrite?: (src: string) => string
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
// Per-target source that a FEATURE package supplies for this target, as
|
|
652
|
+
// `project-relative path -> the file it was copied from`.
|
|
653
|
+
//
|
|
654
|
+
// Only for features whose source folder is not the target's own: everything
|
|
655
|
+
// the target ships for itself is already in the tree being walked, and
|
|
656
|
+
// listing it twice would compare it against itself.
|
|
657
|
+
//
|
|
658
|
+
// Only for ACTIVE features, because that is what `feature add` copies. A
|
|
659
|
+
// feature switched off later leaves its files behind, and those really are
|
|
660
|
+
// stale — reporting them is the point.
|
|
342
661
|
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
662
|
+
// Discovered with `findFeatureSources`, the same function the fan-out and the
|
|
663
|
+
// trim use, so doctor's idea of which files belong to a feature cannot drift
|
|
664
|
+
// from the one that put them there.
|
|
665
|
+
function foreignFeatureSource(
|
|
666
|
+
actx: ActionContext, target: Source,
|
|
667
|
+
): Map<string, string> {
|
|
668
|
+
const out = new Map<string, string>()
|
|
669
|
+
|
|
670
|
+
const features: any = (actx.model as any)?.main?.[KIT]?.feature ?? {}
|
|
671
|
+
|
|
672
|
+
for (const fname of Object.keys(features).sort()) {
|
|
673
|
+
if (false === features[fname]?.active) {
|
|
674
|
+
continue
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const source = resolveDeclared('feature', fname, actx)
|
|
678
|
+
|
|
679
|
+
if (null == source) {
|
|
680
|
+
continue
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const [rel, from] of overlayFiles(actx, source, target)) {
|
|
684
|
+
out.set(rel, from)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return out
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
// One feature's per-target source, as `tm-relative path -> the file it was
|
|
693
|
+
// copied from`, for source the FEATURE package supplies rather than the
|
|
694
|
+
// target.
|
|
695
|
+
//
|
|
696
|
+
// ONE enumerator, used from both directions: the target walk needs these to
|
|
697
|
+
// know they are expected, and the feature check needs them to compare. Two
|
|
698
|
+
// enumerations would be two ideas of which files a feature owns, and the
|
|
699
|
+
// pair would drift — which is the failure this codebase keeps producing.
|
|
700
|
+
//
|
|
701
|
+
// Empty when the feature's source IS the target's own tree: those files are
|
|
702
|
+
// already in the tree being walked, and listing them would compare them
|
|
703
|
+
// against themselves.
|
|
704
|
+
function overlayFiles(
|
|
705
|
+
actx: ActionContext, feature: Source, target: Source,
|
|
706
|
+
): Map<string, string> {
|
|
707
|
+
const fs = actx.fs()
|
|
708
|
+
const out = new Map<string, string>()
|
|
709
|
+
|
|
710
|
+
if (feature.folder === target.folder) {
|
|
711
|
+
return out
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// The feature package's overlay for THIS target, under the name the target
|
|
715
|
+
// has in its own source — an aliased target's templates live at
|
|
716
|
+
// `tm/<origname>`.
|
|
717
|
+
const overlay = Path.join(feature.folder, 'tm', target.origname)
|
|
718
|
+
|
|
719
|
+
for (const found of findFeatureSources(fs, overlay, [feature.name])) {
|
|
720
|
+
const from = Path.join(overlay, found.path)
|
|
721
|
+
|
|
722
|
+
// A folder source is the whole feature directory; expand it, because the
|
|
723
|
+
// comparison is per file.
|
|
724
|
+
if (found.folder) {
|
|
725
|
+
for (const rel of walk(fs, from)) {
|
|
726
|
+
out.set(found.path + '/' + rel, Path.join(from, rel))
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
out.set(found.path, from)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return out
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
// A FEATURE's per-target source, checked from the feature's side.
|
|
739
|
+
//
|
|
740
|
+
// This is where those files are COMPARED. The target walk only marks them
|
|
741
|
+
// expected, so that each is compared exactly once — by the feature that owns
|
|
742
|
+
// it, which is also the only scope in which `feature add` will rewrite it.
|
|
743
|
+
//
|
|
744
|
+
// Doing it the other way round — leaving the comparison in the target walk —
|
|
745
|
+
// left the gate open in the case it was built for: `package update` on a
|
|
746
|
+
// FEATURE package scopes doctor to that feature, no target is walked, and so
|
|
747
|
+
// nothing compared the very overlay the feature package supplies. Verified
|
|
748
|
+
// before fixing: the update succeeded without asking, and the local edit was
|
|
749
|
+
// destroyed.
|
|
750
|
+
function checkFeatureSource(
|
|
751
|
+
actx: ActionContext,
|
|
752
|
+
feature: Source,
|
|
753
|
+
targets: Map<string, Source>,
|
|
754
|
+
report: DoctorReport,
|
|
755
|
+
) {
|
|
355
756
|
const fs = actx.fs()
|
|
757
|
+
const root = actx.folder
|
|
758
|
+
const model = actx.model
|
|
356
759
|
|
|
357
|
-
|
|
760
|
+
for (const [tname, target] of targets) {
|
|
761
|
+
for (const [rel, from] of overlayFiles(actx, feature, target)) {
|
|
762
|
+
const project = Path.join(root, 'tm', tname, rel)
|
|
763
|
+
const label = 'tm/' + tname + '/' + rel
|
|
764
|
+
|
|
765
|
+
if (!fs.existsSync(project)) {
|
|
766
|
+
report.missing.push(label)
|
|
767
|
+
continue
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// The same map `feature add` copies with — see helpers/stdrep. A
|
|
771
|
+
// different one here would report every substituted file as edited.
|
|
772
|
+
if (differs(fs, from, project, model, templateReplacements(model, tname))) {
|
|
773
|
+
report.edited.push(label)
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
358
778
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
779
|
+
|
|
780
|
+
// The copied MODEL FILE — `model/<kind>/<name>.aontu`, written by add with
|
|
781
|
+
// the `'BASE'` replacement, and overwritten on every resync exactly as
|
|
782
|
+
// `src/cmp` and `tm` are.
|
|
783
|
+
//
|
|
784
|
+
// It is not scaffolding trivia. A target's carries its dependency set, its
|
|
785
|
+
// `phase` gates, `srcfeature`, `feature.trim` / `feature.fullset` and its
|
|
786
|
+
// publish-registry identity; a feature's carries its version, `active`
|
|
787
|
+
// default, `config` defaults and hook wiring. So a maintainer who fixes a dep
|
|
788
|
+
// version there loses the fix on the next resync with nothing said (the SDK
|
|
789
|
+
// regresses with nobody touching it, which is the failure the "a project
|
|
790
|
+
// decision belongs in the MODEL" rule exists to prevent), and a project whose
|
|
791
|
+
// copy predates a scaffold change carries the old declaration forever while
|
|
792
|
+
// doctor reports it in sync.
|
|
793
|
+
//
|
|
794
|
+
// KIND-NEUTRAL, because the file is the same kind of thing for every kind:
|
|
795
|
+
// one definition, copied, stamped with provenance, optionally renamed for an
|
|
796
|
+
// alias. Only the alias rewrite is kind-specific, and the registry already
|
|
797
|
+
// says which kinds can be aliased at all.
|
|
798
|
+
function checkItemModel(
|
|
799
|
+
actx: ActionContext, kind: string, source: Source, report: DoctorReport,
|
|
800
|
+
) {
|
|
801
|
+
const name = source.name
|
|
802
|
+
const origname = source.origname
|
|
803
|
+
const base = source.base
|
|
804
|
+
const fs = actx.fs()
|
|
805
|
+
|
|
806
|
+
const scaffold = definitionPath(source.folder, kind, origname)
|
|
807
|
+
|
|
808
|
+
// Nothing to compare against — a source that no longer ships this item.
|
|
366
809
|
if (!fs.existsSync(scaffold)) {
|
|
367
810
|
return
|
|
368
811
|
}
|
|
369
812
|
|
|
370
|
-
|
|
371
|
-
|
|
813
|
+
// An ALIAS's model file is PROJECT-OWNED: `target add go~go2` creates it and
|
|
814
|
+
// never overwrites it again, because differentiating it is the whole point
|
|
815
|
+
// of an alias (a second Go module needs its own module name and deps), and
|
|
816
|
+
// add-a-target tells the project to edit it. So a difference here is not a
|
|
817
|
+
// fork — `target add` will not revert it — and must not fail the check.
|
|
818
|
+
//
|
|
819
|
+
// It is still worth REPORTING, which it never was before: the origin was
|
|
820
|
+
// unrecoverable, so doctor skipped the file entirely and an alias that had
|
|
821
|
+
// drifted far from a moved-on upstream said nothing. With `origname`
|
|
822
|
+
// recorded the comparison is possible, so it runs, with the same key
|
|
823
|
+
// rewrite the add applied — and anything left over is the project's own
|
|
824
|
+
// differentiation, reported as informational.
|
|
825
|
+
//
|
|
826
|
+
// Only for a kind that CAN be aliased. A feature cannot, so for features
|
|
827
|
+
// this is always false and the ownership argument never applies — the
|
|
828
|
+
// registry says which, rather than this inferring it from the names
|
|
829
|
+
// happening to match.
|
|
830
|
+
const aliased = kindDef(kind).alias && name !== origname
|
|
831
|
+
|
|
832
|
+
const project = definitionPath(actx.folder, kind, name)
|
|
833
|
+
const label = 'model/' + kind + '/' + name + '.aontu'
|
|
372
834
|
|
|
373
835
|
if (!fs.existsSync(project)) {
|
|
374
836
|
report.missing.push(label)
|
|
375
837
|
return
|
|
376
838
|
}
|
|
377
839
|
|
|
378
|
-
// The substitution
|
|
840
|
+
// The substitution the add applied on the way in: jostraca's template()
|
|
379
841
|
// against the model (so `module: name: '$$name$$'` arrives as the project
|
|
380
|
-
// slug) plus the
|
|
842
|
+
// slug) plus the provenance block recording where the scaffold was.
|
|
381
843
|
//
|
|
382
844
|
// NOT templateReplacements() — that is the tm/ tree's map. The two writers
|
|
383
|
-
// pass different maps, so doctor has to as well, or every
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
845
|
+
// pass different maps, so doctor has to as well, or every project reads as
|
|
846
|
+
// forked on the `base:` line alone. The map itself is shared with the
|
|
847
|
+
// writer (helpers/stdrep) so the two cannot drift.
|
|
848
|
+
//
|
|
849
|
+
// `package` comes from the SOURCE's manifest, not from what the copy
|
|
850
|
+
// records, for the same reason `base` does: what this compares is what the
|
|
851
|
+
// add would write NOW. A package that renamed itself therefore reads as
|
|
852
|
+
// forked, which is accurate — the next add would rewrite the line.
|
|
853
|
+
const provenance = provenanceReplace(
|
|
854
|
+
{ base, origname, name, package: source.package })
|
|
855
|
+
|
|
856
|
+
// For an alias, compare against what the origin WOULD produce under the new
|
|
857
|
+
// name, so the rename itself is not the difference.
|
|
858
|
+
const rename = kindDef(kind).rename
|
|
859
|
+
|
|
860
|
+
const rewrite = (aliased && null != rename) ?
|
|
861
|
+
(src: string) => rename(src, origname, name) : undefined
|
|
862
|
+
|
|
863
|
+
if (!differs(fs, scaffold, project, actx.model, provenance, undefined, rewrite)) {
|
|
864
|
+
return
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (aliased) {
|
|
868
|
+
report.aliasedDiff.push(label)
|
|
869
|
+
return
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// A copy written before the toolchain stamped some provenance key differs
|
|
873
|
+
// from the scaffold by exactly the lines carrying that key. That is not a
|
|
874
|
+
// fork — the project changed nothing — and reporting it as one would turn
|
|
875
|
+
// every existing consumer's CI red on upgrade.
|
|
876
|
+
//
|
|
877
|
+
// PER KEY, not all-or-nothing. The keys arrived in stages — `base` and
|
|
878
|
+
// `origname` first, `package` with the manifest — so a project that
|
|
879
|
+
// resynced between two of them holds a copy that IS stamped (it has `base`)
|
|
880
|
+
// and is still missing a later key. An all-or-nothing "is it stamped at
|
|
881
|
+
// all" test called every one of those a fork: on the released 3.4.8
|
|
882
|
+
// scaffold only ts, csharp and swift carried the anchor, and `ts` is in
|
|
883
|
+
// essentially every consumer SDK, so that was close to the whole installed
|
|
884
|
+
// base going red on a file nobody touched.
|
|
885
|
+
if (stampOnly(fs, scaffold, project, actx.model, provenance, rewrite)) {
|
|
886
|
+
report.resyncPending.push(label)
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
report.forked.push(label)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
// Is the ONLY difference a provenance line the copy has not been given yet?
|
|
895
|
+
//
|
|
896
|
+
// MATCHED AGAINST THE EXACT LINES THE STAMP PRODUCES, never against a pattern
|
|
897
|
+
// for "a line that looks like provenance". `base`, `origname` and `package`
|
|
898
|
+
// are not reserved words: `main: kit: target: <t>: module: package` (the Go
|
|
899
|
+
// root package identifier) and `publish: registry: package` (the published
|
|
900
|
+
// package name) are declared model slots that a target model may write in
|
|
901
|
+
// block form, on their own line, looking exactly like a provenance line to
|
|
902
|
+
// any regex. A generic pattern got BOTH directions wrong on such a model —
|
|
903
|
+
// it read the copy's `module: package:` as proof the copy was already
|
|
904
|
+
// stamped, so an untouched pre-manifest file was reported as forked; and it
|
|
905
|
+
// stripped that same real line from the comparison, so DELETING it — an
|
|
906
|
+
// actual fork, silently reverted by the next `target add` — was reported as
|
|
907
|
+
// a pending resync and passed the check.
|
|
908
|
+
//
|
|
909
|
+
// Comparing against `provenanceReplace`'s own output has no such ambiguity,
|
|
910
|
+
// and keeps the reader tied to the writer, which is the whole point of
|
|
911
|
+
// helpers/stdrep.
|
|
912
|
+
//
|
|
913
|
+
// The rule, once the two are known to differ:
|
|
914
|
+
//
|
|
915
|
+
// - every line the EXPECTED has and the copy lacks must be a stamp line;
|
|
916
|
+
// - the copy must have NO line the expected lacks.
|
|
917
|
+
//
|
|
918
|
+
// The second half is what keeps the tolerance narrow, as the previous
|
|
919
|
+
// rollout's review required: a changed `package:` value leaves the old line
|
|
920
|
+
// in the copy and unmatched, so it is a fork and is reported as one.
|
|
921
|
+
function stampOnly(
|
|
922
|
+
fs: any, scaffoldPath: string, projectPath: string, model: any,
|
|
923
|
+
provenance: Record<string, string>,
|
|
924
|
+
rewrite?: (src: string) => string,
|
|
925
|
+
): boolean {
|
|
926
|
+
const { expected, actual } = renderPair(
|
|
927
|
+
fs, scaffoldPath, projectPath, model, provenance, rewrite)
|
|
928
|
+
|
|
929
|
+
// The rendered block, as lines: `base: '...'` plus whichever of
|
|
930
|
+
// `origname:` / `package:` applied. Trimmed on both sides of the
|
|
931
|
+
// comparison, because the anchor's own indentation belongs to the scaffold.
|
|
932
|
+
const stamp = new Set(
|
|
933
|
+
Object.values(provenance).join('\n').split('\n')
|
|
934
|
+
.map((s: string) => s.trim()))
|
|
935
|
+
|
|
936
|
+
const onlyExpected = lineDiff(expected, actual)
|
|
937
|
+
const onlyActual = lineDiff(actual, expected)
|
|
938
|
+
|
|
939
|
+
return 0 === onlyActual.length &&
|
|
940
|
+
0 < onlyExpected.length &&
|
|
941
|
+
onlyExpected.every((line: string) => stamp.has(line.trim()))
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
// Lines of `a` that `b` does not have, counting duplicates.
|
|
946
|
+
function lineDiff(a: string, b: string): string[] {
|
|
947
|
+
const pool = new Map<string, number>()
|
|
948
|
+
|
|
949
|
+
for (const line of b.split('\n')) {
|
|
950
|
+
pool.set(line, (pool.get(line) ?? 0) + 1)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const out: string[] = []
|
|
954
|
+
|
|
955
|
+
for (const line of a.split('\n')) {
|
|
956
|
+
const n = pool.get(line) ?? 0
|
|
957
|
+
if (0 < n) {
|
|
958
|
+
pool.set(line, n - 1)
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
out.push(line)
|
|
962
|
+
}
|
|
387
963
|
}
|
|
964
|
+
|
|
965
|
+
return out
|
|
388
966
|
}
|
|
389
967
|
|
|
390
968
|
|
|
@@ -400,7 +978,7 @@ function walk(fs: any, dir: string): string[] {
|
|
|
400
978
|
const descend = (rel: string) => {
|
|
401
979
|
const abs = '' === rel ? dir : Path.join(dir, rel)
|
|
402
980
|
for (const entry of fs.readdirSync(abs).sort()) {
|
|
403
|
-
if (
|
|
981
|
+
if (ignoredEntry(entry)) {
|
|
404
982
|
continue
|
|
405
983
|
}
|
|
406
984
|
const entryrel = '' === rel ? entry : rel + '/' + entry
|
|
@@ -432,27 +1010,59 @@ function excluded(rel: string, excludes: RegExp[]): boolean {
|
|
|
432
1010
|
// Compare a scaffold file with a project file, applying the SAME substitution
|
|
433
1011
|
// `target add` applied on the way in. Without this the comparison reports
|
|
434
1012
|
// every substituted placeholder as an edit.
|
|
1013
|
+
// `ignore` drops matching lines from BOTH sides before comparing, for a
|
|
1014
|
+
// difference that is known not to be a fork (see the provenance rollout in
|
|
1015
|
+
// checkTargetModel). It is deliberately a second, narrower question asked
|
|
1016
|
+
// only after a plain comparison has already found a difference — so a file
|
|
1017
|
+
// that matches exactly never depends on it.
|
|
435
1018
|
function differs(
|
|
436
1019
|
fs: any, scaffoldPath: string, projectPath: string, model: any, replace: any,
|
|
1020
|
+
ignore?: (line: string) => boolean,
|
|
1021
|
+
rewrite?: (src: string) => string,
|
|
437
1022
|
): boolean {
|
|
438
1023
|
if (BINARY_RE.test(scaffoldPath)) {
|
|
439
1024
|
return !fs.readFileSync(scaffoldPath).equals(fs.readFileSync(projectPath))
|
|
440
1025
|
}
|
|
441
1026
|
|
|
442
|
-
const
|
|
1027
|
+
const { expected, actual } = renderPair(
|
|
1028
|
+
fs, scaffoldPath, projectPath, model, replace, rewrite)
|
|
1029
|
+
|
|
1030
|
+
if (null == ignore) {
|
|
1031
|
+
return expected !== actual
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const strip = (s: string) =>
|
|
1035
|
+
s.split('\n').filter((line: string) => !ignore(line)).join('\n')
|
|
1036
|
+
|
|
1037
|
+
return strip(expected) !== strip(actual)
|
|
1038
|
+
}
|
|
443
1039
|
|
|
444
|
-
// template() runs even when there is nothing to REPLACE, because jostraca's
|
|
445
|
-
// Copy always interpolates `$$ref$$` against the model as well — the replace
|
|
446
|
-
// map is an extra, not the whole substitution. Skipping it for the src/cmp
|
|
447
|
-
// tree (whose Copy passes no replace map) meant the three Config fragments
|
|
448
|
-
// that carry `$$const.Name$$` / `$$main.kit.info.servers.0.url$$` arrived
|
|
449
|
-
// substituted and compared as bytes: every project with ts, js or dart
|
|
450
|
-
// reported `src/cmp/<t>/fragment/Config.fragment.<ext>` as FORKED straight
|
|
451
|
-
// out of `target add`, which is precisely the noise this function exists to
|
|
452
|
-
// remove. With no `$$` refs and no replace keys, template() is the identity.
|
|
453
|
-
const expected = template(src, model, { replace })
|
|
454
1040
|
|
|
455
|
-
|
|
1041
|
+
// What `target add` WOULD write, beside what the project actually has.
|
|
1042
|
+
//
|
|
1043
|
+
// One definition, shared by the byte comparison and the stamp-only tolerance,
|
|
1044
|
+
// so the two can never disagree about what the expected text is.
|
|
1045
|
+
//
|
|
1046
|
+
// template() runs even when there is nothing to REPLACE, because jostraca's
|
|
1047
|
+
// Copy always interpolates `$$ref$$` against the model as well — the replace
|
|
1048
|
+
// map is an extra, not the whole substitution. Skipping it for the src/cmp
|
|
1049
|
+
// tree (whose Copy passes no replace map) meant the three Config fragments
|
|
1050
|
+
// that carry `$$const.Name$$` / `$$main.kit.info.servers.0.url$$` arrived
|
|
1051
|
+
// substituted and compared as bytes: every project with ts, js or dart
|
|
1052
|
+
// reported `src/cmp/<t>/fragment/Config.fragment.<ext>` as FORKED straight
|
|
1053
|
+
// out of `target add`, which is precisely the noise this exists to remove.
|
|
1054
|
+
// With no `$$` refs and no replace keys, template() is the identity.
|
|
1055
|
+
function renderPair(
|
|
1056
|
+
fs: any, scaffoldPath: string, projectPath: string, model: any, replace: any,
|
|
1057
|
+
rewrite?: (src: string) => string,
|
|
1058
|
+
): { expected: string, actual: string } {
|
|
1059
|
+
const rawsrc = fs.readFileSync(scaffoldPath, 'utf8')
|
|
1060
|
+
const src = null == rewrite ? rawsrc : rewrite(rawsrc)
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
expected: template(src, model, { replace }),
|
|
1064
|
+
actual: fs.readFileSync(projectPath, 'utf8'),
|
|
1065
|
+
}
|
|
456
1066
|
}
|
|
457
1067
|
|
|
458
1068
|
|
|
@@ -467,6 +1077,7 @@ function quietLog(log: any): any {
|
|
|
467
1077
|
|
|
468
1078
|
export type {
|
|
469
1079
|
DoctorReport,
|
|
1080
|
+
DoctorScope,
|
|
470
1081
|
}
|
|
471
1082
|
|
|
472
1083
|
export {
|