carrick 0.3.111 → 0.3.113

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.
Files changed (41) hide show
  1. package/bin/carrick.mjs +3 -0
  2. package/dist/contract.d.ts +6 -0
  3. package/dist/contract.js +5 -0
  4. package/dist/contract.js.map +1 -1
  5. package/dist/hook/post-edit.js +11 -2
  6. package/dist/hook/post-edit.js.map +1 -1
  7. package/dist/hook/post-read.d.ts +2 -0
  8. package/dist/hook/post-read.js +89 -0
  9. package/dist/hook/post-read.js.map +1 -0
  10. package/dist/hook/reuse.d.ts +24 -0
  11. package/dist/hook/reuse.js +58 -2
  12. package/dist/hook/reuse.js.map +1 -1
  13. package/dist/init/settings.js +6 -0
  14. package/dist/init/settings.js.map +1 -1
  15. package/dist/render.d.ts +22 -2
  16. package/dist/render.js +53 -2
  17. package/dist/render.js.map +1 -1
  18. package/dist/update.js +3 -0
  19. package/dist/update.js.map +1 -1
  20. package/package.json +62 -7
  21. package/plugin/.claude-plugin/plugin.json +1 -1
  22. package/plugin/hooks/hooks.json +10 -0
  23. package/scripts/install-scanned-deps.sh +539 -0
  24. package/sidecar/dist/src/capture/api.d.ts +22 -1
  25. package/sidecar/dist/src/capture/check-probe.d.ts +8 -0
  26. package/sidecar/dist/src/capture/check-probe.js +9 -1
  27. package/sidecar/dist/src/capture/check.d.ts +32 -0
  28. package/sidecar/dist/src/capture/check.js +74 -5
  29. package/sidecar/dist/src/capture/checkout.d.ts +38 -0
  30. package/sidecar/dist/src/capture/checkout.js +139 -0
  31. package/sidecar/dist/src/capture/index.d.ts +1 -0
  32. package/sidecar/dist/src/capture/index.js +22 -7
  33. package/sidecar/dist/src/capture/machinery.js +1 -30
  34. package/sidecar/dist/src/capture/project-references.js +2 -1
  35. package/sidecar/dist/src/capture/self-check.d.ts +20 -1
  36. package/sidecar/dist/src/capture/self-check.js +30 -11
  37. package/sidecar/dist/src/origin.js +1 -29
  38. package/sidecar/dist/src/project-loader.d.ts +6 -0
  39. package/sidecar/dist/src/project-loader.js +64 -1
  40. package/sidecar/dist/src/type-inferrer.d.ts +235 -1
  41. package/sidecar/dist/src/type-inferrer.js +901 -111
@@ -12,7 +12,7 @@
12
12
  import { Project, getCompilerOptionsFromTsConfig } from 'ts-morph';
13
13
  import * as path from 'node:path';
14
14
  import * as fs from 'node:fs';
15
- import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath, sidecarCompilerOptions, } from './capture/index.js';
15
+ import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath, sidecarCompilerOptions, untrustedReadBound, } from './capture/index.js';
16
16
  import { moduleFormatResolutionHost } from './module-format.js';
17
17
  import { ExternalImports, registerExternalImports } from './origin.js';
18
18
  /**
@@ -177,6 +177,7 @@ export class ProjectLoader {
177
177
  this.log('Project will load with tsconfig snapshot');
178
178
  this.buildProject = () => {
179
179
  const project = new Project({
180
+ ...this.fileSystemOption(),
180
181
  compilerOptions: sidecarCompilerOptions(this.snapshotToCompilerOptions(snapshot)),
181
182
  skipAddingFilesFromTsConfig: true,
182
183
  });
@@ -197,6 +198,7 @@ export class ProjectLoader {
197
198
  // rebuilds that drop them (carrick#1731).
198
199
  const imports = new ExternalImports();
199
200
  const project = new Project({
201
+ ...this.fileSystemOption(),
200
202
  compilerOptions: sidecarCompilerOptions(deno.parsed.options),
201
203
  skipAddingFilesFromTsConfig: true,
202
204
  resolutionHost: imports.recording((host, getOptions) => ({
@@ -221,6 +223,7 @@ export class ProjectLoader {
221
223
  this.log('No tsconfig.json found, using default compiler options');
222
224
  this.buildProject = () => {
223
225
  const project = new Project({
226
+ ...this.fileSystemOption(),
224
227
  compilerOptions: sidecarCompilerOptions(DEFAULT_COMPILER_OPTIONS),
225
228
  skipAddingFilesFromTsConfig: true,
226
229
  });
@@ -318,10 +321,20 @@ export class ProjectLoader {
318
321
  }
319
322
  return result;
320
323
  }
324
+ /**
325
+ * Under CARRICK_UNTRUSTED_TREE, the file system every project reads
326
+ * through: nothing whose real path is outside the checkout (carrick#2230).
327
+ * Nothing at all otherwise, so a project is built as it always was.
328
+ */
329
+ fileSystemOption() {
330
+ const bound = untrustedReadBound(this.repoRoot);
331
+ return bound ? { fileSystem: boundedFileSystem(bound) } : {};
332
+ }
321
333
  /** A ts-morph project built from one tsconfig and the files it lists. */
322
334
  projectFromConfig(configPath) {
323
335
  const imports = new ExternalImports();
324
336
  const project = new Project({
337
+ ...this.fileSystemOption(),
325
338
  tsConfigFilePath: configPath,
326
339
  // Over the tsconfig's own options: ts-morph lays these on top.
327
340
  compilerOptions: sidecarCompilerOptions(getCompilerOptionsFromTsConfig(configPath).options),
@@ -492,3 +505,53 @@ export class ProjectLoader {
492
505
  console.error(`[sidecar:project-loader:error] ${message}`);
493
506
  }
494
507
  }
508
+ /**
509
+ * A ts-morph file system that reads only inside the bound. ts-morph reads the
510
+ * tsconfig, lists the files it names and reads each through this host, and its
511
+ * module resolution asks it whether a file exists. Writes are not bounded:
512
+ * the write guard owns them (carrick#1748).
513
+ */
514
+ function boundedFileSystem(bound) {
515
+ const base = new Project({
516
+ compilerOptions: sidecarCompilerOptions({}),
517
+ skipAddingFilesFromTsConfig: true,
518
+ skipFileDependencyResolution: true,
519
+ }).getFileSystem();
520
+ const missing = (filePath) => Object.assign(new Error(`ENOENT: no such file or directory, open '${filePath}'`), { code: 'ENOENT' });
521
+ return new Proxy(base, {
522
+ get(target, prop, receiver) {
523
+ const value = Reflect.get(target, prop, receiver);
524
+ if (typeof value !== 'function')
525
+ return value;
526
+ const fn = value.bind(target);
527
+ switch (prop) {
528
+ case 'readFileSync':
529
+ return (p, e) => {
530
+ if (!bound.allows(p))
531
+ throw missing(p);
532
+ return fn(p, e);
533
+ };
534
+ case 'readFile':
535
+ return async (p, e) => {
536
+ if (!bound.allows(p))
537
+ throw missing(p);
538
+ return fn(p, e);
539
+ };
540
+ case 'fileExistsSync':
541
+ case 'directoryExistsSync':
542
+ return (p) => bound.allows(p) && fn(p);
543
+ case 'fileExists':
544
+ case 'directoryExists':
545
+ return async (p) => bound.allows(p) && (await fn(p));
546
+ case 'readDirSync':
547
+ return (p) => (bound.allows(p) ? fn(p) : []);
548
+ case 'globSync':
549
+ return (patterns) => fn(patterns).filter((f) => bound.allows(f));
550
+ case 'glob':
551
+ return async (patterns) => (await fn(patterns)).filter((f) => bound.allows(f));
552
+ default:
553
+ return fn;
554
+ }
555
+ },
556
+ });
557
+ }
@@ -38,6 +38,21 @@ import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js
38
38
  * test (`machinery-indicator-mirror.test.ts`) asserts the two sets stay equal.
39
39
  */
40
40
  export declare const MACHINERY_MEMBER_INDICATORS: Set<string>;
41
+ /**
42
+ * `text` is `string`, alone or beside `null` and `undefined` in either order:
43
+ * the only payload a raw-text read publishes (carrick#1842).
44
+ */
45
+ /**
46
+ * Source text with every plain string literal written in double quotes:
47
+ * `'/a'`, `"/a"` and an interpolation-free `` `/a` `` are one value, so text
48
+ * that differs only in its quotes compares equal (carrick#2056).
49
+ *
50
+ * The text is read left to right, pairing quotes the way the scanner does,
51
+ * so a template's closing backtick never pairs with the next template's
52
+ * opening one. A literal holding a quote, a backslash or an interpolation is
53
+ * copied as it is, and so is an unterminated one with everything after it.
54
+ */
55
+ export declare function canonicalStringQuotes(text: string): string;
41
56
  /**
42
57
  * Options for TypeInferrer construction
43
58
  */
@@ -385,6 +400,11 @@ export declare class TypeInferrer {
385
400
  * belongs to a read.
386
401
  */
387
402
  private typeNodeStatedAtRead;
403
+ /**
404
+ * A wrapper between a body read and the binding it initialises that leaves
405
+ * the read's value as it is: `await`, parentheses, `!`, a cast, `satisfies`.
406
+ */
407
+ private wrapsReadTransparently;
388
408
  /**
389
409
  * A stated type with `any` or `unknown` written anywhere in it (`unknown`,
390
410
  * `Record<string, unknown>`, `{ items: any[] }`) leaves a position open: it
@@ -924,6 +944,180 @@ export declare class TypeInferrer {
924
944
  * return statements are.
925
945
  */
926
946
  private recoverPayloadFromResponseExpressions;
947
+ /** How one payload node reads: its stated annotation, else its resolved type. */
948
+ private payloadCandidateOf;
949
+ /** A payload whose contract a type node in the source states. */
950
+ private statedPayloadCandidate;
951
+ /**
952
+ * Join the payloads of several response branches into one contract, as
953
+ * several return statements are joined: an empty object drops beside a
954
+ * branch with members, equal texts dedupe, and the rest is a union.
955
+ */
956
+ private joinPayloadCandidates;
957
+ /**
958
+ * True when a handler's awaited return, or a member of its union, is a
959
+ * serialised-body envelope: an object with a numeric `statusCode` or
960
+ * `status` member and a `body` member typed `string`. Either member may be
961
+ * optional, as a platform's result type declares them.
962
+ *
963
+ * HTTP vocabulary only, the class `RESPONSE_INIT_MEMBER_NAMES` is. A `body`
964
+ * typed as an object is not an envelope: a runtime that serialises the
965
+ * handler's return value sends that object as the payload, so the shape is
966
+ * ambiguous and keeps the ordinary reading.
967
+ */
968
+ private typeIsSerialisedBodyEnvelope;
969
+ /** A member's type with `undefined` and `null` removed, or `undefined` if absent. */
970
+ private envelopeMemberType;
971
+ /**
972
+ * True when a handler's return is a serialised-body envelope AND the source
973
+ * shows the body is serialised, so the envelope reading applies:
974
+ * - a returned branch builds an envelope whose `body` is `JSON.stringify`
975
+ * of a value, inline, through a `const`, or in a builder the reading can
976
+ * follow (`branchShowsSerialisation`); or
977
+ * - the envelope type is declared outside the repository (a platform's
978
+ * result type) with every member optional. Nothing else returns such a
979
+ * type, so a value of it returned as-is is still transport. The repo's
980
+ * own all-optional types (`Partial<Article>`) engage only on evidence.
981
+ *
982
+ * A domain object that merely has `status` and `body` members (an article,
983
+ * a message) shows neither and keeps the ordinary reading.
984
+ */
985
+ private serialisedBodyEnvelopeEngages;
986
+ /**
987
+ * True when a returned branch builds an envelope from a serialised value:
988
+ * a literal whose `body` is `JSON.stringify(...)`, a call to a builder the
989
+ * reading can follow, or a call handing on one such envelope.
990
+ */
991
+ private branchShowsSerialisation;
992
+ /**
993
+ * The arguments a call could hand back as its result: every argument but
994
+ * an object literal, which is the call's input (`create({ body:
995
+ * JSON.stringify(doc) })` stores a document, it does not respond with it),
996
+ * and but a function, whose returns are read as the call's callbacks.
997
+ */
998
+ private handedOnArguments;
999
+ /**
1000
+ * The returned branches of an envelope handler: each returned expression,
1001
+ * conditional sides split. A returned call whose callbacks return values
1002
+ * (`settle().then(ok, fail)`) is carried by those returns, plus every
1003
+ * envelope it is handed directly (`withFallback(() => a, b)` can answer
1004
+ * `b`); a call whose callbacks return nothing stays a branch of its own.
1005
+ */
1006
+ private envelopeBranches;
1007
+ /**
1008
+ * The `function_return` answer for a handler that returns a serialised-body
1009
+ * envelope: the union of the values its success branches serialised.
1010
+ *
1011
+ * Each returned branch is read by `readEnvelopeBranch`. Error and redirect
1012
+ * branches contribute nothing (carrick#1161). If any branch cannot be read,
1013
+ * the answer is `unknown` with the decided reason `serialised_body_unread`
1014
+ * and one sentence per cause: the branches that did read are part of the
1015
+ * contract, and neither the envelope nor its `string` body is any of it.
1016
+ */
1017
+ private inferSerialisedBodyEnvelope;
1018
+ /** An `unknown` answer the inferrer decided, carrying its reason at the root. */
1019
+ private decidedAbstain;
1020
+ /**
1021
+ * What one returned branch of an envelope handler serialised:
1022
+ * - an envelope object literal: the argument of the `JSON.stringify` that
1023
+ * produced its `body`, inline or through a `const` in the same function;
1024
+ * its status member decides the branch;
1025
+ * - a call to a response builder this repository declares
1026
+ * (`envelopeBuilderOf`): the argument the builder serialises; the
1027
+ * argument or literal its status member takes decides the branch;
1028
+ * - any other call handing on exactly one envelope argument (never an
1029
+ * object literal, which is the call's input): that
1030
+ * envelope is read in its place. Its own stated status decides the
1031
+ * branch; only when it states none does a status the outer call's
1032
+ * arguments state decide (`log(ctx, 503, "code", prebuilt)`).
1033
+ * Anything else, and a payload holding `any` or `unknown`, is unread, with
1034
+ * its cause.
1035
+ */
1036
+ private readEnvelopeBranch;
1037
+ /** The status a builder call sends: its status argument, or the builder's literal. */
1038
+ private builderCallStatus;
1039
+ /**
1040
+ * The status an envelope expression states itself: a literal's status
1041
+ * member, or what a followable builder call sends. `'undecided'` for
1042
+ * anything else (a variable holding an envelope states nothing here).
1043
+ */
1044
+ private envelopeOwnStatus;
1045
+ /** The status an envelope literal's `statusCode` or `status` member states. */
1046
+ private envelopeLiteralStatus;
1047
+ /** The status a value states, classified as a send's status argument is. */
1048
+ private statusOfValue;
1049
+ /**
1050
+ * The value node an object literal gives a member: a property assignment's
1051
+ * initializer, or a shorthand's own name.
1052
+ */
1053
+ private envelopeMemberValue;
1054
+ /**
1055
+ * The value an envelope literal's `body` serialises: the argument of a
1056
+ * `JSON.stringify` written there, or written as the initializer of a
1057
+ * `const` that `scope` declares and `body` names. `undefined` when `body`
1058
+ * is built any other way.
1059
+ */
1060
+ private serialisedBodyArgument;
1061
+ /**
1062
+ * The initializer of the `const` an identifier (or a shorthand member's
1063
+ * name) refers to, when `scope` itself declares it.
1064
+ */
1065
+ private constInitializerInScope;
1066
+ /**
1067
+ * The symbol of the value a node names: a shorthand member's value symbol
1068
+ * (its own symbol is the property), else the identifier's symbol.
1069
+ */
1070
+ private valueSymbolOf;
1071
+ /** The index of the `params` entry a node names, or `undefined`. */
1072
+ private parameterIndexOf;
1073
+ /**
1074
+ * How a call's callee builds an envelope, when the callee is a function this
1075
+ * repository declares (not lib, not an installed package) and every return
1076
+ * it makes is an envelope literal:
1077
+ * - returns whose status member is a literal 4xx or 5xx are its failure
1078
+ * path and are ignored; a builder with nothing else is `'failure'`, so a
1079
+ * call to it is an error branch;
1080
+ * - every other return must serialise the same parameter into `body` and
1081
+ * take its status from the same parameter, or state the same literal
1082
+ * success or redirect status.
1083
+ * `undefined` when the callee is anything else.
1084
+ */
1085
+ private envelopeBuilderOf;
1086
+ /**
1087
+ * The single function a callee symbol declares with a body: a function
1088
+ * declaration, or a variable initialised with an arrow or function
1089
+ * expression.
1090
+ */
1091
+ private builderFunctionOf;
1092
+ /**
1093
+ * The payload a builder call serialises. The source states it, and the
1094
+ * answer is explicit, when the argument carries `satisfies`/`as`, when the
1095
+ * builder's parameter is one of its type parameters and the call passes
1096
+ * type arguments (`reply<AddNoteResult>(201, x)`), or when the parameter is
1097
+ * declared with a type that says something. Otherwise the argument's own
1098
+ * type is read.
1099
+ */
1100
+ private builderPayloadCandidate;
1101
+ /**
1102
+ * True when a declared parameter type does not say what is sent: a top
1103
+ * type, the `object` keyword, an object type that names no member (`{}`,
1104
+ * `Record<string, unknown>`), a union that admits both primitives and
1105
+ * objects (a JSON-value type such as `string | number | Json[] | { [k:
1106
+ * string]: Json }`), or a type that mentions a type parameter. A builder
1107
+ * that takes "any value" states nothing about the payload a call hands it,
1108
+ * so the argument's own type is read instead.
1109
+ */
1110
+ private typeNodeStatesNothing;
1111
+ /** A serialised value's candidate, unread when it holds a top type. */
1112
+ private envelopePayloadCandidate;
1113
+ /**
1114
+ * A payload candidate, or unread when its printed type is or holds `any`
1115
+ * or `unknown` anywhere (`{ [x: string]: any }`, `{ data: any }`). A
1116
+ * contract with a hole in it is not published on this path: the capture
1117
+ * would refuse the text and re-read the line, which can publish the
1118
+ * envelope.
1119
+ */
1120
+ private checkedEnvelopeCandidate;
927
1121
  /**
928
1122
  * The request field each member of a joined union answers
929
1123
  * (`response-modes.ts`). Case texts are joined by the published union's
@@ -1133,6 +1327,19 @@ export declare class TypeInferrer {
1133
1327
  * followed to a small depth; anything else is returned as itself.
1134
1328
  */
1135
1329
  private expandResponseBranches;
1330
+ /**
1331
+ * The symbol an anchor is named from: the type's own symbol, unless that
1332
+ * symbol's name is synthetic (`__type`, `__object`, ...), in which case the
1333
+ * alias symbol (carrick#2148).
1334
+ *
1335
+ * `type Widget = { id: string }` and an instantiated `Page<Order>` carry
1336
+ * the anonymous `__type` as their own symbol and the alias as their alias
1337
+ * symbol. Reading the own symbol first and stopping because it is truthy
1338
+ * left every object-literal alias without an anchor, while the same shape
1339
+ * declared as an interface had one. The own symbol still wins whenever it
1340
+ * has a real name, so every type anchored before keeps its anchor.
1341
+ */
1342
+ private anchorSymbolOf;
1136
1343
  /**
1137
1344
  * The deterministic source symbol of a resolved type (`Payment` for a payload
1138
1345
  * typed `Payment`), or `undefined` when there is no single user-defined
@@ -1154,7 +1361,8 @@ export declare class TypeInferrer {
1154
1361
  * declarations IN its `source_file`, so the request must point at the file
1155
1362
  * that actually declares the tsc-witnessed payload type.
1156
1363
  *
1157
- * The declarations read are those of the type's own symbol, so a name
1364
+ * The declarations read are those of the symbol the anchor is named from
1365
+ * (`anchorSymbolOf`: the type's own symbol, or its alias's), so a name
1158
1366
  * imported through a barrel reports the file that declares it, and a name
1159
1367
  * two files declare reports the one this type resolves to. A declaration in
1160
1368
  * an installed package or a TypeScript lib is reported as it is found, as
@@ -1460,6 +1668,18 @@ export declare class TypeInferrer {
1460
1668
  * The first such call in source order: a body is read once.
1461
1669
  */
1462
1670
  private platformBodyReadIn;
1671
+ /**
1672
+ * The platform body read whose result `located` is a MEMBER of, or
1673
+ * `undefined` (carrick#2057).
1674
+ *
1675
+ * A member is a property or element access off a binding, or a name
1676
+ * destructured out of one. The binding must be initialised by the body read
1677
+ * `platformBodyReadIn` finds in the function that declares it, with only
1678
+ * transparent wrappers between the two (`await`, parentheses, `!`, a cast,
1679
+ * `satisfies`). The binding itself is not a member, and neither is a member
1680
+ * of anything else (a query object, a parameter, an outbound response).
1681
+ */
1682
+ private parsedBodyReadHolding;
1463
1683
  /**
1464
1684
  * The request contract the source states AT a body read, or null when it
1465
1685
  * states none there (carrick#807).
@@ -1888,6 +2108,20 @@ export declare class TypeInferrer {
1888
2108
  * then substring match (preferring containing matches).
1889
2109
  */
1890
2110
  private matchByText;
2111
+ /**
2112
+ * What a locator's text names, read off its own parse (carrick#2056):
2113
+ *
2114
+ * - `texts`: the normalized text of the expression it names and of each
2115
+ * wrapper it carries around it (`return`, a `const x =` binding, a label,
2116
+ * `await`, parentheses, a cast, `satisfies`, `!`). A node strictly inside
2117
+ * that expression is not among them;
2118
+ * - `rootKind` and `rootStart`: the innermost of those, its kind, and where
2119
+ * its text starts in the locator.
2120
+ *
2121
+ * `null` when the text does not parse as one expression or one statement:
2122
+ * there is no structure to read, and the caller keeps its text rule.
2123
+ */
2124
+ private locatorNames;
1891
2125
  /**
1892
2126
  * Pick the best match from a set of candidate nodes:
1893
2127
  * smallest range, then closest to target line.