Repository: FHIR/sushi (ts)

Issue:
Problems w/ indented ^ rules in RuleSets applied to CodeSystems
This is similar to #833, but also uses indentation to establish the `^` rules within a specific code context.  When you attempt to do that, you get errors like:

> error Errors parsing insert rule with parameterized RuleSet ConceptWithGermanSynonym
> - The first rule of a definition cannot be indented. The rule will be processed as if it is not indented.
and

> error The element or path you referenced does not exist: designation.language

Example:
```
Alias:   $SCT = http://snomed.info/sct

CodeSystem: SimpleSystem
Title:     "Simple System"
Description: "A simple system"
* insert ConceptWithGermanSynonym(
    #Fish,
    "Fish",
    "It is a fish",
    "Fisch")
* insert ConceptWithGermanSynonym(
    #Seal,
    "Seal",
    "It is a seal",
    "Seehund")

RuleSet: ConceptWithGermanSynonym(code, display, definition, synonym)
* {code} {display} {definition}
  * ^designation.language = #de
  * ^designation.use = $SCT#900000000000013009 "Synonym"
  * ^designation.value = {synonym}
```

https://fshschool.org/FSHOnline/#/share/3wbdgpB

Pull request description:
Merge CodeCaretValueRule with CaretValueRule
Fixes #834 by eliminating conflicts between `CaretValueRule` and `CodeCaretValueRule`. The referenced bug happened because on a `RuleSet`, there was no way for us to tell if a rule was a `CaretValueRule`, or a `CodeCaretValueRule` when there was no path before the `^`. The grammar is organized such that a `CaretValueRule` was always preferred in this case, so in the example:
```
* {code} {display} {definition}
  * ^designation.language = #de
```
the first line (once params were added) was processed as a `ConceptRule`, and set context in the `codePathContext` array, but then the second line was processed as a `CaretValueRule`, and so would attempt to get context from the `pathContext` array, find no context (since it was looking in the wrong spot) and then cause errors.

I played around with trying to just select which context to use based on where the `RuleSet` was being `inserted`, but this actually doesn't work if the `RuleSet` has no parameters, because in that case we don't know where the `RuleSet` is `inserted` until the export side. So in the end the easiest solution seemed to be just refactoring so that `CodeCaretValueRule` is removed, and all of its functionality is handled by `CaretValueRule`. This refactor ended up being bigger than I thought, but I _think_ it was an improvement, more code went away than got added so that's a good sign. But please let me know if you think this refactor made things worse, not better.

Note that I left `codeCaretValueRule` in the grammar, because I think on the import side it is still easier to handle these separately from `caretValueRule`s (even though both grammar rules end up being stored in the same `CaretValueRule` class).

Patch:
diff --git a/src/export/CodeSystemExporter.ts b/src/export/CodeSystemExporter.ts
index f37e2fb..4178511 100644
--- a/src/export/CodeSystemExporter.ts
+++ b/src/export/CodeSystemExporter.ts
@@ -2,7 +2,7 @@ import { FSHTank } from '../import/FSHTank';
 import { CodeSystem, CodeSystemConcept, StructureDefinition } from '../fhirtypes';
 import { setPropertyOnInstance, applyInsertRules } from '../fhirtypes/common';
 import { FshCodeSystem } from '../fshtypes';
-import { CaretValueRule, ConceptRule, CodeCaretValueRule } from '../fshtypes/rules';
+import { CaretValueRule, ConceptRule } from '../fshtypes/rules';
 import { logger } from '../utils/FSHLogger';
 import { MasterFisher, Type, resolveSoftIndexing } from '../utils';
 import { Package } from '.';
@@ -62,22 +62,21 @@ export class CodeSystemExporter {
   private setCaretPathRules(
     codeSystem: CodeSystem,
     csStructureDefinition: StructureDefinition,
-    rules: (CaretValueRule | CodeCaretValueRule)[]
+    rules: CaretValueRule[]
   ) {
     // soft index resolution relies on the rule's path attribute.
-    // a CodeCaretValueRule is created with an empty path, so first
-    // transform its codePath into a path.
+    // a CaretValueRule is created with an empty path, so first
+    // transform its arrayPath into a path.
     // Because this.findConceptPath can potentially throw an error,
     // build a list of successful rules that will actually be applied.
-    const successfulRules: (CaretValueRule | CodeCaretValueRule)[] = [];
+    const successfulRules: CaretValueRule[] = [];
     rules.forEach(rule => {
       try {
-        if (rule instanceof CodeCaretValueRule) {
-          rule.path = this.findConceptPath(codeSystem, rule.codePath);
-          successfulRules.push(rule);
-        } else {
-          successfulRules.push(rule);
-        }
+        rule.path = this.findConceptPath(codeSystem, rule.pathArray);
+        successfulRules.push(rule);
+        // We can only know that a CaretRule is meant to represent codes
+        // after it has been applied on a CodeSystem, so set the flag here
+        rule.isCodeCaretRule = true;
       } catch (e) {
         logger.error(e.message, rule.sourceInfo);
       }
@@ -165,9 +164,7 @@ export class CodeSystemExporter {
     this.setCaretPathRules(
       codeSystem,
       csStructureDefinition,
-      fshDefinition.rules.filter(
-        rule => rule instanceof CaretValueRule || rule instanceof CodeCaretValueRule
-      ) as (CaretValueRule | CodeCaretValueRule)[]
+      fshDefinition.rules.filter(rule => rule instanceof CaretValueRule) as CaretValueRule[]
     );
 
     // check for another code system with the same id
diff --git a/src/fshtypes/AllowedRules.ts b/src/fshtypes/AllowedRules.ts
index 5bcd1b1..8a00267 100644
--- a/src/fshtypes/AllowedRules.ts
+++ b/src/fshtypes/AllowedRules.ts
@@ -22,8 +22,7 @@ import {
   ConceptRule,
   ValueSetComponentRule,
   MappingRule,
-  Rule,
-  CodeCaretValueRule
+  Rule
 } from './rules';
 
 const allowedRulesMap = new Map<any, any[]>([
@@ -55,7 +54,7 @@ const allowedRulesMap = new Map<any, any[]>([
   ],
   ['Instance', [AssignmentRule]],
   ['FshValueSet', [ValueSetComponentRule, CaretValueRule]],
-  ['FshCodeSystem', [ConceptRule, CaretValueRule, CodeCaretValueRule]],
+  ['FshCodeSystem', [ConceptRule, CaretValueRule]],
   ['Mapping', [MappingRule]],
   [
     'RuleSet',
@@ -71,8 +70,7 @@ const allowedRulesMap = new Map<any, any[]>([
       ObeysRule,
       OnlyRule,
       ValueSetComponentRule,
-      BindingRule,
-      CodeCaretValueRule
+      BindingRule
     ]
   ],
   [
diff --git a/src/fshtypes/FshCodeSystem.ts b/src/fshtypes/FshCodeSystem.ts
index ab425f9..2623369 100644
--- a/src/fshtypes/FshCodeSystem.ts
+++ b/src/fshtypes/FshCodeSystem.ts
@@ -1,7 +1,7 @@
 import { FshEntity } from './FshEntity';
 import { CodeSystemDuplicateCodeError } from '../errors/CodeSystemDuplicateCodeError';
 import { CodeSystemIncorrectHierarchyError } from '../errors/CodeSystemIncorrectHierarchyError';
-import { CaretValueRule, InsertRule, ConceptRule, CodeCaretValueRule } from './rules';
+import { CaretValueRule, InsertRule, ConceptRule } from './rules';
 import { EOL } from 'os';
 import { fshifyString } from './common';
 import isEqual from 'lodash/isEqual';
@@ -14,7 +14,7 @@ export class FshCodeSystem extends FshEntity {
   id: string;
   title?: string;
   description?: string;
-  rules: (ConceptRule | CaretValueRule | CodeCaretValueRule | InsertRule)[];
+  rules: (ConceptRule | CaretValueRule | InsertRule)[];
 
   constructor(public name: string) {
     super();
diff --git a/src/fshtypes/rules/CaretValueRule.ts b/src/fshtypes/rules/CaretValueRule.ts
index 54ed8a6..a8b7335 100644
--- a/src/fshtypes/rules/CaretValueRule.ts
+++ b/src/fshtypes/rules/CaretValueRule.ts
@@ -11,6 +11,8 @@ export class CaretValueRule extends Rule {
   caretPath: string;
   value: AssignmentValueType;
   isInstance: boolean;
+  isCodeCaretRule = false;
+  pathArray: string[] = [];
 
   constructor(path: string) {
     super(path);
@@ -41,6 +43,14 @@ export class CaretValueRule extends Rule {
     } else if (this.value) {
       value = this.value._instanceMeta.name;
     }
-    return `* ${this.path !== '' ? this.path + ' ' : ''}^${this.caretPath} = ${value}`;
+    let printablePath;
+    if (this.isCodeCaretRule) {
+      printablePath = this.pathArray.length
+        ? this.pathArray.map(code => `#${code}`).join(' ') + ' '
+        : '';
+    } else {
+      printablePath = this.path !== '' ? this.path + ' ' : '';
+    }
+    return `* ${printablePath}^${this.caretPath} = ${value}`;
   }
 }
diff --git a/src/fshtypes/rules/CodeCaretValueRule.ts b/src/fshtypes/rules/CodeCaretValueRule.ts
deleted file mode 100644
index 07d8c3c..0000000
--- a/src/fshtypes/rules/CodeCaretValueRule.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Rule } from './Rule';
-import { AssignmentValueType } from './AssignmentRule';
-import { FshCanonical } from '../FshCanonical';
-import { FshCode } from '../FshCode';
-import { FshQuantity } from '../FshQuantity';
-import { FshRatio } from '../FshRatio';
-import { FshReference } from '../FshReference';
-import { fshifyString } from '../common';
-
-export class CodeCaretValueRule extends Rule {
-  caretPath: string;
-  value: AssignmentValueType;
-  isInstance: boolean;
-
-  constructor(public readonly codePath: string[]) {
-    super('');
-  }
-
-  get constructorName() {
-    return 'CodeCaretValueRule';
-  }
-
-  toFSH(): string {
-    let value;
-    if (
-      this.value instanceof FshCanonical ||
-      this.value instanceof FshCode ||
-      this.value instanceof FshQuantity ||
-      this.value instanceof FshRatio ||
-      this.value instanceof FshReference
-    ) {
-      value = this.value.toString();
-    } else if (
-      typeof this.value === 'boolean' ||
-      typeof this.value === 'number' ||
-      typeof this.value === 'bigint'
-    ) {
-      value = this.value;
-    } else if (typeof this.value === 'string') {
-      value = this.isInstance ? this.value : `"${fshifyString(this.value)}"`;
-    } else if (this.value) {
-      value = this.value._instanceMeta.name;
-    }
-    const printablePath = this.codePath.map(code => `#${code}`).join(' ');
-    return `* ${printablePath} ^${this.caretPath} = ${value}`;
-  }
-}
diff --git a/src/fshtypes/rules/index.ts b/src/fshtypes/rules/index.ts
index 8857456..65683d1 100644
--- a/src/fshtypes/rules/index.ts
+++ b/src/fshtypes/rules/index.ts
@@ -11,7 +11,6 @@ export * from './MappingRule';
 export * from './InsertRule';
 export * from './ValueSetComponentRule';
 export * from './ConceptRule';
-export * from './CodeCaretValueRule';
 export * from './AddElementRule';
 export * from './SdRule';
 export * from './LrRule';
diff --git a/src/import/FSHImporter.ts b/src/import/FSHImporter.ts
index dfe3bcd..593ead2 100644
--- a/src/import/FSHImporter.ts
+++ b/src/import/FSHImporter.ts
@@ -50,9 +50,9 @@ import {
   SdRule,
   LrRule,
   AddElementRule,
-  OnlyRuleType,
-  CodeCaretValueRule
+  OnlyRuleType
 } from '../fshtypes/rules';
+import { splitOnPathPeriods } from '../fhirtypes/common';
 import { ParserRuleContext, InputStream, CommonTokenStream } from 'antlr4';
 import { logger, switchToSecretLogger, LoggerData, restoreMainLogger } from '../utils/FSHLogger';
 import { TerminalNode } from 'antlr4/tree/Tree';
@@ -143,8 +143,7 @@ export class FSHImporter extends FSHVisitor {
   private allAliases: Map<string, string>;
   paramRuleSets: Map<string, ParamRuleSet>;
   private topLevelParse: boolean;
-  private pathContext: string[];
-  private codePathContext: string[][];
+  private pathContext: string[][];
   private baseIndent: number;
 
   constructor() {
@@ -244,9 +243,8 @@ export class FSHImporter extends FSHVisitor {
   }
 
   visitEntity(ctx: pc.EntityContext): void {
-    // Reset the pathContext, codePathContext, and baseIndent level for each entity
+    // Reset the pathContext and baseIndent level for each entity
     this.pathContext = [];
-    this.codePathContext = [];
     this.baseIndent = this.extractStartStop(ctx).startColumn;
 
     if (ctx.profile()) {
@@ -1097,9 +1095,7 @@ export class FSHImporter extends FSHVisitor {
     }
   }
 
-  visitCsRule(
-    ctx: pc.CsRuleContext
-  ): ConceptRule | CodeCaretValueRule | CaretValueRule | InsertRule {
+  visitCsRule(ctx: pc.CsRuleContext): ConceptRule | CaretValueRule | InsertRule {
     if (ctx.concept()) {
       return this.visitConcept(ctx.concept());
     } else if (ctx.codeCaretValueRule()) {
@@ -1130,12 +1126,15 @@ export class FSHImporter extends FSHVisitor {
     }
   }
 
-  getPathWithContext(path: string, parentCtx: ParserRuleContext) {
-    return this.prependPathContext(path, parentCtx);
+  getPathWithContext(path: string, parentCtx: ParserRuleContext): string {
+    return this.getArrayPathWithContext(
+      splitOnPathPeriods(path).filter(p => p),
+      parentCtx
+    ).join('.');
   }
 
-  getCodePathWithContext(codePath: string[], parentCtx: ParserRuleContext) {
-    return this.prependCodePathContext(codePath, parentCtx);
+  getArrayPathWithContext(pathArray: string[], parentCtx: ParserRuleContext): string[] {
+    return this.prependPathContext(pathArray, parentCtx);
   }
 
   visitPath(ctx: pc.PathContext): string {
@@ -1343,7 +1342,7 @@ export class FSHImporter extends FSHVisitor {
     // the last code in allCodes is the one we are actually defining.
     // the rest are the hierarchy, which may be empty.
     // indentation may also be used to define the hierarchy, which is what the code path with context is for.
-    const fullCodePath = this.getCodePathWithContext(
+    const fullCodePath = this.getArrayPathWithContext(
       localCodePath.map(localCode => localCode.code),
       ctx
     );
@@ -1543,12 +1542,16 @@ export class FSHImporter extends FSHVisitor {
   }
 
   visitCaretValueRule(ctx: pc.CaretValueRuleContext): CaretValueRule {
-    const caretValueRule = new CaretValueRule(
-      this.getPathWithContext(this.visitPath(ctx.path()), ctx)
-    )
+    const path = this.visitPath(ctx.path());
+    const splitPath =
+      path === '.' ? [path] : splitOnPathPeriods(this.visitPath(ctx.path())).filter(p => p);
+    const pathArray = this.getArrayPathWithContext(splitPath, ctx);
+    const caretValueRule = new CaretValueRule(pathArray.join('.'))
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
-
+    // We must save the path in array form, in case this rule is a part
+    // of a RuleSet which ends up applied on a CodeSystem
+    caretValueRule.pathArray = pathArray;
     // Get the caret path, but slice off the starting ^
     caretValueRule.caretPath = this.visitCaretPath(ctx.caretPath()).slice(1);
     caretValueRule.value = this.visitValue(ctx.value());
@@ -1557,17 +1560,18 @@ export class FSHImporter extends FSHVisitor {
     return caretValueRule;
   }
 
-  visitCodeCaretValueRule(ctx: pc.CodeCaretValueRuleContext): CodeCaretValueRule {
+  visitCodeCaretValueRule(ctx: pc.CodeCaretValueRuleContext): CaretValueRule {
     const localCodePath = ctx.CODE()
       ? ctx.CODE().map(code => {
           return this.parseCodeLexeme(code.getText(), ctx).code;
         })
       : [];
-    const fullCodePath = this.getCodePathWithContext(localCodePath, ctx);
+    const fullCodePath = this.getArrayPathWithContext(localCodePath, ctx);
     // It's fine to make a CodeCaretValueRule with an empty code path.
-    const caretRule = new CodeCaretValueRule(fullCodePath)
+    const caretRule = new CaretValueRule('')
       .withLocation(this.extractStartStop(ctx))
       .withFile(this.currentFile);
+    caretRule.pathArray = fullCodePath;
     // Get the caret path, but slice off the starting ^
     caretRule.caretPath = this.visitCaretPath(ctx.caretPath()).slice(1);
     caretRule.value = this.visitValue(ctx.value());
@@ -2014,9 +2018,9 @@ export class FSHImporter extends FSHVisitor {
    * Given a path and the context containing it, apply the path context indicated by the path's indent
    * @param path - The path to apply context to
    * @param parentCtx - The parent element containing the path
-   * @returns {string} - The path with context prepended
+   * @returns {string[]} - The path with context prepended
    */
-  private prependPathContext(path: string, parentCtx: any): string {
+  private prependPathContext(path: string[], parentCtx: ParserRuleContext): string[] {
     const location = this.extractStartStop(parentCtx);
     const currentIndent = location.startColumn - this.baseIndent;
     const contextIndex = currentIndent / INDENT_WIDTH;
@@ -2026,15 +2030,19 @@ export class FSHImporter extends FSHVisitor {
     }
 
     // Replace '[+]' with '[=]' in the version of the path to add to pathContext, unless the rule is only used to set a path
-    const newContext = pc.hasPathRule(parentCtx.parentCtx) ? path : path.replace(/\[\+\]/g, '[=]');
+    const newContext = pc.hasPathRule(parentCtx.parentCtx)
+      ? path
+      : path.map(c => c.replace(/\[\+\]/g, '[=]'));
+
     // If the element is not indented, just reset the context
     if (contextIndex === 0) {
       this.pathContext = [newContext];
       return path;
     }
 
+    // Otherwise, get the context based on the indent level.
     const currentContext = this.pathContext[contextIndex - 1];
-    if (currentContext === '') {
+    if (currentContext.length === 0) {
       logger.error(
         'Rule cannot be indented below rule which has no path. The rule will be processed as if it is not indented.',
         { location, file: this.currentFile }
@@ -2042,55 +2050,18 @@ export class FSHImporter extends FSHVisitor {
       return path;
     }
 
+    // Trim out-of-scope contexts
     this.pathContext.splice(contextIndex);
     // Once we have used the existing context, clear it of any [+] so that a rule that is only setting path only applies [+] once
     if (this.pathContext.length > 0) {
       this.pathContext[this.pathContext.length - 1] = this.pathContext[
         this.pathContext.length - 1
-      ].replace(/\[\+\]/g, '[=]');
+      ].map(c => c.replace(/\[\+\]/g, '[=]'));
     }
-    this.pathContext.push(newContext ? `${currentContext}.${newContext}` : currentContext);
-    return path ? `${currentContext}.${path}` : currentContext;
-  }
-
-  /**
-   * Given a code path and the context containing it, apply the code path context indicated by the code path's indent
-   * @param codePath - The code path to apply context to
-   * @param parentCtx - The parent element containing the code path
-   * @returns {string[]} - The code path with context prepended
-   */
-  private prependCodePathContext(codePath: string[], parentCtx: ParserRuleContext): string[] {
-    const location = this.extractStartStop(parentCtx);
-    const currentIndent = location.startColumn - this.baseIndent;
-    const contextIndex = currentIndent / INDENT_WIDTH;
-
-    if (!this.isValidContext(location, currentIndent, this.codePathContext)) {
-      return codePath;
-    }
-
-    // A PathRule can set a path, but there's no equivalent for a code path.
-    // And, there's no need to check for soft indexing either.
-    // If there's no indent, use the codePath as-is, and make it the only available context.
-    if (contextIndex === 0) {
-      this.codePathContext = [codePath];
-      return codePath;
-    }
-    // Otherwise, get the context based on the indent level.
-    const currentContext = this.codePathContext[contextIndex - 1];
-    if (currentContext.length === 0) {
-      logger.error(
-        'Rule cannot be indented below rule which has no code path. The rule will be processed as if it is not indented.',
-        { location, file: this.currentFile }
-      );
-      return codePath;
-    }
-
-    // Trim out-of-scope contexts
-    this.codePathContext.splice(contextIndex);
-    const fullCodePath = currentContext.concat(codePath);
-    this.codePathContext.push(fullCodePath);
+    const fullPath = currentContext.concat(newContext);
+    this.pathContext.push(fullPath);
 
-    return fullCodePath;
+    return fullPath;
   }
 
   private isValidContext(
diff --git a/src/utils/PathUtils.ts b/src/utils/PathUtils.ts
index 892ed4d..ff787cc 100644
--- a/src/utils/PathUtils.ts
+++ b/src/utils/PathUtils.ts
@@ -1,6 +1,6 @@
 import { PathPart } from '../fhirtypes';
 import { splitOnPathPeriods } from '../fhirtypes/common';
-import { CaretValueRule, Rule, CodeCaretValueRule } from '../fshtypes/rules';
+import { CaretValueRule, Rule } from '../fshtypes/rules';
 import { logger } from './FSHLogger';
 
 /**
@@ -119,8 +119,8 @@ export function resolveSoftIndexing(rules: Array<Rule | CaretValueRule>): void {
     const parsedPath: { path: PathPart[]; caretPath?: PathPart[] } = {
       path: parseFSHPath(rule.path)
     };
-    // If we have a CaretValueRule or CodeCaretValueRule, we'll need a second round of parsing for the caret path
-    if (rule instanceof CaretValueRule || rule instanceof CodeCaretValueRule) {
+    // If we have a CaretValueRule, we'll need a second round of parsing for the caret path
+    if (rule instanceof CaretValueRule) {
       parsedPath.caretPath = parseFSHPath(rule.caretPath);
     }
     return parsedPath;
@@ -158,7 +158,7 @@ export function resolveSoftIndexing(rules: Array<Rule | CaretValueRule>): void {
     });
 
     // If a rule is a CaretValueRule, we assemble its caretPath as well
-    if (originalRule instanceof CaretValueRule || originalRule instanceof CodeCaretValueRule) {
+    if (originalRule instanceof CaretValueRule) {
       originalRule.caretPath = assembleFSHPath(parsedRule.caretPath);
     }
   });


Test patch:
diff --git a/test/export/CodeSystemExporter.test.ts b/test/export/CodeSystemExporter.test.ts
index 66be84b..baff741 100644
--- a/test/export/CodeSystemExporter.test.ts
+++ b/test/export/CodeSystemExporter.test.ts
@@ -1,13 +1,7 @@
 import { CodeSystemExporter, Package } from '../../src/export';
 import { FSHDocument, FSHTank } from '../../src/import';
 import { FshCodeSystem, FshCode, RuleSet } from '../../src/fshtypes';
-import {
-  CaretValueRule,
-  InsertRule,
-  AssignmentRule,
-  ConceptRule,
-  CodeCaretValueRule
-} from '../../src/fshtypes/rules';
+import { CaretValueRule, InsertRule, AssignmentRule, ConceptRule } from '../../src/fshtypes/rules';
 import { TestFisher } from '../testhelpers';
 import { loggerSpy } from '../testhelpers';
 import { FHIRDefinitions, loadFromPath } from '../../src/fhirdefs';
@@ -323,10 +317,11 @@ describe('CodeSystemExporter', () => {
     });
   });
 
-  it('should apply a CodeCaretValueRule on a top-level concept', () => {
+  it('should apply a CaretValueRule on a top-level concept', () => {
     const codeSystem = new FshCodeSystem('CaretCodeSystem');
     const someCode = new ConceptRule('someCode', 'Some Code');
-    const someCaret = new CodeCaretValueRule(['someCode']);
+    const someCaret = new CaretValueRule('');
+    someCaret.pathArray = ['someCode'];
     someCaret.caretPath = 'designation[0].value';
     someCaret.value = 'Designated value';
     codeSystem.rules.push(someCode, someCaret);
@@ -356,12 +351,13 @@ describe('CodeSystemExporter', () => {
     });
   });
 
-  it('should apply a CodeCaretValueRule on a concept within a hierarchy', () => {
+  it('should apply a CaretValueRule on a concept within a hierarchy', () => {
     const codeSystem = new FshCodeSystem('CaretCodeSystem');
     const someCode = new ConceptRule('someCode', 'Some Code');
     const otherCode = new ConceptRule('otherCode', 'Other Code');
     otherCode.hierarchy = ['someCode'];
-    const someCaret = new CodeCaretValueRule(['someCode', 'otherCode']);
+    const someCaret = new CaretValueRule('');
+    someCaret.pathArray = ['someCode', 'otherCode'];
     someCaret.caretPath = 'designation[0].value';
     someCaret.value = 'Other designated value';
     codeSystem.rules.push(someCode, otherCode, someCaret);
@@ -397,7 +393,7 @@ describe('CodeSystemExporter', () => {
     });
   });
 
-  it('should resolve soft indexing when applying Caret Value rules', () => {
+  it('should resolve soft indexing when applying top level Caret Value rules', () => {
     const codeSystem = new FshCodeSystem('CaretCodeSystem');
     const contactRule1 = new CaretValueRule('');
     contactRule1.caretPath = 'contact[+].name';
@@ -428,23 +424,27 @@ describe('CodeSystemExporter', () => {
     ]);
   });
 
-  it('should resolve soft indexing when applying CodeCaretValue rules', () => {
+  it('should resolve soft indexing when applying CaretValue rules with paths', () => {
     const codeSystem = new FshCodeSystem('CodeCaretCS');
     const topCode = new ConceptRule('topCode', 'Top Code');
     const bottomCode = new ConceptRule('bottomCode', 'Bottom Code');
     bottomCode.hierarchy = ['topCode'];
 
-    const firstTop = new CodeCaretValueRule(['topCode']);
+    const firstTop = new CaretValueRule('');
+    firstTop.pathArray = ['topCode'];
     firstTop.caretPath = 'designation[+].value';
     firstTop.value = 'First top designation';
-    const secondTop = new CodeCaretValueRule(['topCode']);
+    const secondTop = new CaretValueRule('');
+    secondTop.pathArray = ['topCode'];
     secondTop.caretPath = 'designation[+].value';
     secondTop.value = 'Second top designation';
 
-    const firstBottom = new CodeCaretValueRule(['topCode', 'bottomCode']);
+    const firstBottom = new CaretValueRule('');
+    firstBottom.pathArray = ['topCode', 'bottomCode'];
     firstBottom.caretPath = 'designation[+].value';
     firstBottom.value = 'First bottom designation';
-    const secondBottom = new CodeCaretValueRule(['topCode', 'bottomCode']);
+    const secondBottom = new CaretValueRule('');
+    secondBottom.pathArray = ['topCode', 'bottomCode'];
     secondBottom.caretPath = 'designation[+].value';
     secondBottom.value = 'Second bottom designation';
 
@@ -608,14 +608,15 @@ describe('CodeSystemExporter', () => {
     expect(loggerSpy.getLastMessage('error')).toMatch(/File: InvalidValue\.fsh.*Line: 6\D*/s);
   });
 
-  it('should log a message when applying an invalid CodeCaretValueRule', () => {
+  it('should log a message when applying an invalid CaretValueRule', () => {
     const codeSystem = new FshCodeSystem('CaretCodeSystem');
     const someCode = new ConceptRule('someCode', 'Some Code');
     const otherCode = new ConceptRule('otherCode', 'Other Code');
     otherCode.hierarchy = ['someCode'];
-    const someCaret = new CodeCaretValueRule(['someCode', 'wrongCode'])
+    const someCaret = new CaretValueRule('')
       .withFile('InvalidValue.fsh')
       .withLocation([8, 5, 8, 25]);
+    someCaret.pathArray = ['someCode', 'wrongCode'];
     someCaret.caretPath = 'designation[0].value';
     someCaret.value = 'Other designated value';
     codeSystem.rules.push(someCode, otherCode, someCaret);
diff --git a/test/fshtypes/AllowedRules.test.ts b/test/fshtypes/AllowedRules.test.ts
index 5a5844c..985785e 100644
--- a/test/fshtypes/AllowedRules.test.ts
+++ b/test/fshtypes/AllowedRules.test.ts
@@ -14,8 +14,7 @@ import {
   InsertRule,
   ValueSetComponentRule,
   ValueSetConceptComponentRule,
-  ValueSetFilterComponentRule,
-  CodeCaretValueRule
+  ValueSetFilterComponentRule
 } from '../../src/fshtypes/rules';
 import {
   isAllowedRule,
@@ -189,7 +188,6 @@ describe('isAllowedRule', () => {
     it('should allow valid rules on a FshCodeSystem', () => {
       expect(isAllowedRule(c, new CaretValueRule('foo'))).toBeTrue();
       expect(isAllowedRule(c, new ConceptRule('foo'))).toBeTrue();
-      expect(isAllowedRule(c, new CodeCaretValueRule(['foo']))).toBeTrue();
     });
 
     it('should not allow invalid rules on a FshCodeSystem', () => {
@@ -249,7 +247,6 @@ describe('isAllowedRule', () => {
       expect(isAllowedRule(r, new MappingRule('foo'))).toBeTrue();
       expect(isAllowedRule(r, new ValueSetComponentRule(true))).toBeTrue();
       expect(isAllowedRule(r, new ConceptRule('foo'))).toBeTrue();
-      expect(isAllowedRule(r, new CodeCaretValueRule(['foo']))).toBeTrue();
     });
 
     it('should not allow invalid rules on a RuleSet', () => {
diff --git a/test/fshtypes/rules/CaretValueRule.test.ts b/test/fshtypes/rules/CaretValueRule.test.ts
index 6fffd25..f00eb1c 100644
--- a/test/fshtypes/rules/CaretValueRule.test.ts
+++ b/test/fshtypes/rules/CaretValueRule.test.ts
@@ -178,5 +178,34 @@ describe('CaretValueRule', () => {
 
       expect(rule.toFSH()).toBe('* ^contact = MyContact');
     });
+
+    it('should produce FSH for a CaretValueRule with isCodeCaretRule assigning a string', () => {
+      const rule = new CaretValueRule('');
+      rule.pathArray = ['bear'];
+      rule.isCodeCaretRule = true;
+      rule.caretPath = 'display';
+      rule.value = 'Bear is "here"';
+
+      expect(rule.toFSH()).toBe('* #bear ^display = "Bear is \\"here\\""');
+    });
+
+    it('should produce FSH for a nested CaretValueRule with isCodeCaretRule assigning a FshCanonical', () => {
+      const rule = new CaretValueRule('');
+      rule.pathArray = ['owl', 'barnowl'];
+      rule.isCodeCaretRule = true;
+      rule.caretPath = 'extension.url';
+      rule.value = new FshCanonical('OwlExtension');
+
+      expect(rule.toFSH()).toBe('* #owl #barnowl ^extension.url = Canonical(OwlExtension)');
+    });
+
+    it('should produce FSH for CaretValueRule with isCodeCaretRule and no path', () => {
+      const rule = new CaretValueRule('');
+      rule.isCodeCaretRule = true;
+      rule.caretPath = 'extension.url';
+      rule.value = new FshCanonical('OwlExtension');
+
+      expect(rule.toFSH()).toBe('* ^extension.url = Canonical(OwlExtension)');
+    });
   });
 });
diff --git a/test/fshtypes/rules/CodeCaretValueRule.test.ts b/test/fshtypes/rules/CodeCaretValueRule.test.ts
deleted file mode 100644
index 8f7c3b8..0000000
--- a/test/fshtypes/rules/CodeCaretValueRule.test.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import 'jest-extended';
-import { InstanceDefinition } from '../../../src/fhirtypes';
-import { FshCanonical, FshCode, FshQuantity, FshRatio, FshReference } from '../../../src/fshtypes';
-import { CodeCaretValueRule } from '../../../src/fshtypes/rules/CodeCaretValueRule';
-
-describe('CodeCaretValueRule', () => {
-  describe('#constructor', () => {
-    it('should set the properties correctly', () => {
-      const c = new CodeCaretValueRule(['first', 'another']);
-      expect(c.path).toBe('');
-      expect(c.codePath).toEqual(['first', 'another']);
-      expect(c.caretPath).toBeUndefined();
-      expect(c.value).toBeUndefined();
-      expect(c.isInstance).toBeUndefined();
-    });
-  });
-
-  describe('#toFSH', () => {
-    it('should produce FSH for a CodeCaretValueRule assigning a string', () => {
-      const rule = new CodeCaretValueRule(['bear']);
-      rule.caretPath = 'display';
-      rule.value = 'Bear is "here"';
-
-      expect(rule.toFSH()).toBe('* #bear ^display = "Bear is \\"here\\""');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning an instance', () => {
-      const rule = new CodeCaretValueRule(['bear', 'sunbear']);
-      rule.caretPath = 'contained[0]';
-      rule.isInstance = true;
-      rule.value = 'Foo';
-
-      expect(rule.toFSH()).toBe('* #bear #sunbear ^contained[0] = Foo');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a boolean', () => {
-      const rule = new CodeCaretValueRule(['goat']);
-      rule.caretPath = 'property[0].valueBoolean';
-      rule.value = false;
-
-      expect(rule.toFSH()).toBe('* #goat ^property[0].valueBoolean = false');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a number', () => {
-      const rule = new CodeCaretValueRule(['owl']);
-      rule.caretPath = 'property[1].valueInteger';
-      rule.value = 7;
-
-      expect(rule.toFSH()).toBe('* #owl ^property[1].valueInteger = 7');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshCanonical', () => {
-      const rule = new CodeCaretValueRule(['owl', 'barnowl']);
-      rule.caretPath = 'extension.url';
-      rule.value = new FshCanonical('OwlExtension');
-
-      expect(rule.toFSH()).toBe('* #owl #barnowl ^extension.url = Canonical(OwlExtension)');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshCanonical with a version', () => {
-      const rule = new CodeCaretValueRule(['owl', 'barnowl']);
-      rule.caretPath = 'extension.url';
-      rule.value = new FshCanonical('OwlExtension');
-      rule.value.version = '1.2.3';
-
-      expect(rule.toFSH()).toBe('* #owl #barnowl ^extension.url = Canonical(OwlExtension|1.2.3)');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshCode with just a code', () => {
-      const rule = new CodeCaretValueRule(['shark']);
-      rule.caretPath = 'property[0].valueCode';
-      rule.value = new FshCode('rough');
-
-      expect(rule.toFSH()).toBe('* #shark ^property[0].valueCode = #rough');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshCode with a code and system', () => {
-      const rule = new CodeCaretValueRule(['shark']);
-      rule.caretPath = 'property[0].valueCode';
-      rule.value = new FshCode('rough', 'http://example.com');
-
-      expect(rule.toFSH()).toBe('* #shark ^property[0].valueCode = http://example.com#rough');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshCode with a code, system, and display', () => {
-      const rule = new CodeCaretValueRule(['shark']);
-      rule.caretPath = 'property[0].valueCode';
-      rule.value = new FshCode('rough', 'http://example.com', 'rough skin');
-
-      expect(rule.toFSH()).toBe(
-        '* #shark ^property[0].valueCode = http://example.com#rough "rough skin"'
-      );
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshQuantity', () => {
-      const rule = new CodeCaretValueRule(['crocodile']);
-      rule.caretPath = 'extension[0].value[x].maxValueQuantity';
-      rule.value = new FshQuantity(15);
-
-      expect(rule.toFSH()).toBe('* #crocodile ^extension[0].value[x].maxValueQuantity = 15');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshQuantity with a unit', () => {
-      const rule = new CodeCaretValueRule(['crocodile']);
-      rule.caretPath = 'extension[0].value[x].maxValueQuantity';
-      const meters = new FshCode('m', 'http://unitsofmeasure.org');
-      rule.value = new FshQuantity(15, meters);
-
-      expect(rule.toFSH()).toBe("* #crocodile ^extension[0].value[x].maxValueQuantity = 15 'm'");
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshQuantity with a non-UCUM system', () => {
-      const rule = new CodeCaretValueRule(['crocodile']);
-      rule.caretPath = 'extension[0].value[x].maxValueQuantity';
-      const meters = new FshCode('m', 'http://otherunits.org', 'meters');
-      rule.value = new FshQuantity(15, meters);
-
-      expect(rule.toFSH()).toBe(
-        '* #crocodile ^extension[0].value[x].maxValueQuantity = 15 http://otherunits.org#m "meters"'
-      );
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshRatio', () => {
-      const rule = new CodeCaretValueRule(['alligator']);
-      rule.caretPath = 'extension[0].value[x].patternRatio';
-      const numerator = new FshQuantity(130);
-      const denominator = new FshQuantity(1);
-      rule.value = new FshRatio(numerator, denominator);
-
-      expect(rule.toFSH()).toBe('* #alligator ^extension[0].value[x].patternRatio = 130 : 1');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshRatio with units', () => {
-      const rule = new CodeCaretValueRule(['alligator']);
-      rule.caretPath = 'extension[0].value[x].patternRatio';
-      const mg = new FshCode('mg', 'http://unitsofmeasure.org');
-      const numerator = new FshQuantity(130, mg);
-      const dL = new FshCode('dL', 'http://unitsofmeasure.org');
-      const denominator = new FshQuantity(1, dL);
-      rule.value = new FshRatio(numerator, denominator);
-
-      expect(rule.toFSH()).toBe(
-        "* #alligator ^extension[0].value[x].patternRatio = 130 'mg' : 1 'dL'"
-      );
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshReference', () => {
-      const rule = new CodeCaretValueRule(['leopard']);
-      rule.caretPath = 'url';
-      rule.value = new FshReference('Example');
-
-      expect(rule.toFSH()).toBe('* #leopard ^url = Reference(Example)');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning a FshReference with display', () => {
-      const rule = new CodeCaretValueRule(['leopard']);
-      rule.caretPath = 'url';
-      rule.value = new FshReference('Example', 'My Example');
-
-      expect(rule.toFSH()).toBe('* #leopard ^url = Reference(Example) "My Example"');
-    });
-
-    it('should produce FSH for a CodeCaretValueRule assigning an InstanceDefinition', () => {
-      const rule = new CodeCaretValueRule(['corvid']);
-      rule.caretPath = 'contact';
-      rule.value = new InstanceDefinition();
-      rule.value._instanceMeta.name = 'MyContact';
-
-      expect(rule.toFSH()).toBe('* #corvid ^contact = MyContact');
-    });
-  });
-});
diff --git a/test/import/FSHImporter.CodeSystem.test.ts b/test/import/FSHImporter.CodeSystem.test.ts
index efdad81..808347e 100644
--- a/test/import/FSHImporter.CodeSystem.test.ts
+++ b/test/import/FSHImporter.CodeSystem.test.ts
@@ -1,5 +1,5 @@
 import { importSingleText } from '../testhelpers/importSingleText';
-import { assertConceptRule, assertInsertRule, assertCodeCaretRule } from '../testhelpers/asserts';
+import { assertConceptRule, assertInsertRule, assertCaretValueRule } from '../testhelpers/asserts';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { Rule, CaretValueRule, InsertRule, ConceptRule } from '../../src/fshtypes/rules';
 
@@ -412,21 +412,19 @@ describe('FSHImporter', () => {
         expect(codeSystem.rules).toHaveLength(0);
         expect(loggerSpy.getLastMessage('error')).toMatch(/File: Simple\.fsh.*Line: 3\D*/s);
       });
-    });
 
-    describe('#codeCaretValueRule', () => {
-      it('should parse a code system that uses a CodeCaretValueRule with no codes', () => {
+      it('should parse a code system that uses a CaretValueRule with no codes', () => {
         const input = `
           CodeSystem: ZOO
           * ^publisher = "Matt"
           `;
         const result = importSingleText(input);
         const codeSystem = result.codeSystems.get('ZOO');
-        assertCodeCaretRule(codeSystem.rules[0] as Rule, [], 'publisher', 'Matt', false);
+        assertCaretValueRule(codeSystem.rules[0] as Rule, '', 'publisher', 'Matt', false, []);
         expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
       });
 
-      it('should parse a code system that uses CodeCaretValueRules with no codes alongside rules', () => {
+      it('should parse a code system that uses CaretValueRules with no codes alongside rules', () => {
         const input = `
         CodeSystem: ZOO
         * #lion
@@ -436,11 +434,18 @@ describe('FSHImporter', () => {
         const codeSystem = result.codeSystems.get('ZOO');
         assertConceptRule(codeSystem.rules[0], 'lion', undefined, undefined, []);
         expect(codeSystem.rules[0].sourceInfo.file).toBe('Zoo.fsh');
-        assertCodeCaretRule(codeSystem.rules[1] as CaretValueRule, [], 'publisher', 'Damon', false);
+        assertCaretValueRule(
+          codeSystem.rules[1] as CaretValueRule,
+          '',
+          'publisher',
+          'Damon',
+          false,
+          []
+        );
         expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
       });
 
-      it('should parse a code system that uses a CodeCaretValueRule on a top-level concept', () => {
+      it('should parse a code system that uses a CaretValueRule on a top-level concept', () => {
         const input = `
         CodeSystem: ZOO
         * #anteater "Anteater"
@@ -450,17 +455,19 @@ describe('FSHImporter', () => {
         const codeSystem = result.codeSystems.get('ZOO');
         assertConceptRule(codeSystem.rules[0], 'anteater', 'Anteater', undefined, []);
         expect(codeSystem.rules[0].sourceInfo.file).toBe('Zoo.fsh');
-        assertCodeCaretRule(
+        assertCaretValueRule(
           codeSystem.rules[1],
-          ['anteater'],
+          '',
           'property[0].valueString',
-          'Their threat pose is really cute.'
+          'Their threat pose is really cute.',
+          false,
+          ['anteater']
         );
         expect(codeSystem.rules[1].sourceInfo.file).toBe('Zoo.fsh');
         expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
       });
 
-      it('should parse a code system that uses a CodeCaretValueRule on a nested concept', () => {
+      it('should parse a code system that uses a CaretValueRule on a nested concept', () => {
         const input = `
         CodeSystem: ZOO
         * #anteater "Anteater"
@@ -475,11 +482,13 @@ describe('FSHImporter', () => {
           'anteater'
         ]);
         expect(codeSystem.rules[1].sourceInfo.file).toBe('Zoo.fsh');
-        assertCodeCaretRule(
+        assertCaretValueRule(
           codeSystem.rules[2],
-          ['anteater', 'northern'],
+          '',
           'property[0].valueString',
-          'They are strong climbers.'
+          'They are strong climbers.',
+          false,
+          ['anteater', 'northern']
         );
         expect(codeSystem.rules[2].sourceInfo.file).toBe('Zoo.fsh');
         expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
diff --git a/test/import/FSHImporter.Profile.test.ts b/test/import/FSHImporter.Profile.test.ts
index 97fb1a1..1570ffa 100644
--- a/test/import/FSHImporter.Profile.test.ts
+++ b/test/import/FSHImporter.Profile.test.ts
@@ -1827,6 +1827,17 @@ describe('FSHImporter', () => {
         );
       });
 
+      it('should parse caret value rules with a . path', () => {
+        const input = `
+        Profile: ObservationProfile
+        Parent: Observation
+        * . ^definition = "foo"
+        `;
+        const result = importSingleText(input);
+        const profile = result.profiles.get('ObservationProfile');
+        assertCaretValueRule(profile.rules[0], '.', 'definition', 'foo', false, ['.']);
+      });
+
       it('should parse caret value rules with a path', () => {
         const input = `
         Profile: ObservationProfile
diff --git a/test/import/FSHImporter.RuleSet.test.ts b/test/import/FSHImporter.RuleSet.test.ts
index 79f692c..085549b 100644
--- a/test/import/FSHImporter.RuleSet.test.ts
+++ b/test/import/FSHImporter.RuleSet.test.ts
@@ -6,7 +6,7 @@ import {
   assertInsertRule,
   assertValueSetConceptComponent,
   assertAddElementRule,
-  assertCodeCaretRule
+  assertCaretValueRule
 } from '../testhelpers/asserts';
 import { loggerSpy } from '../testhelpers/loggerSpy';
 import { Rule, ConceptRule } from '../../src/fshtypes/rules';
@@ -133,7 +133,7 @@ describe('FSHImporter', () => {
       });
     });
 
-    it('should parse a RuleSet with rules, ValueSetComponents, ConceptRules, and CodeCaretValueRules', () => {
+    it('should parse a RuleSet with rules, ValueSetComponents, ConceptRules, and CaretValueRules', () => {
       const input = `
         RuleSet: RuleRuleSet
         * gender from https://www.hl7.org/fhir/valueset-administrative-gender.html
@@ -165,11 +165,13 @@ describe('FSHImporter', () => {
       expect(concept).toEqual(
         new ConceptRule('lion').withFile('Rules.fsh').withLocation([5, 9, 5, 15])
       );
-      assertCodeCaretRule(
+      assertCaretValueRule(
         ruleSet.rules[3],
-        ['lion'],
+        '',
         'designation.value',
-        'Watch out for big cat!'
+        'Watch out for big cat!',
+        false,
+        ['lion']
       );
       expect(ruleSet.rules[3].sourceInfo.location).toEqual({
         startLine: 6,
diff --git a/test/import/FSHImporter.context.test.ts b/test/import/FSHImporter.context.test.ts
index 6aae0ba..bf63e85 100644
--- a/test/import/FSHImporter.context.test.ts
+++ b/test/import/FSHImporter.context.test.ts
@@ -10,7 +10,6 @@ import {
   assertObeysRule,
   assertValueSetConceptComponent,
   assertValueSetFilterComponent,
-  assertCodeCaretRule,
   assertAddElementRule
 } from '../testhelpers/asserts';
 import { FshCode } from '../../src/fshtypes';
@@ -229,7 +228,7 @@ describe('FSHImporter', () => {
       expect(profile.parent).toBe('Patient');
       expect(profile.rules.length).toBe(3);
       assertCardRule(profile.rules[0], 'name', 1, 1);
-      assertCaretValueRule(profile.rules[1], 'name', 'short', 'foo', false);
+      assertCaretValueRule(profile.rules[1], 'name', 'short', 'foo', false, ['name']);
       assertObeysRule(profile.rules[2], 'name', 'inv1');
     });
 
@@ -307,7 +306,7 @@ describe('FSHImporter', () => {
         undefined,
         undefined
       );
-      assertCaretValueRule(profile.rules[2], 'birthDate', 'short', 'foo', false);
+      assertCaretValueRule(profile.rules[2], 'birthDate', 'short', 'foo', false, ['birthDate']);
     });
 
     it('should apply context to AddElement rules', () => {
@@ -576,7 +575,7 @@ describe('FSHImporter', () => {
       expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
     });
 
-    it('should parse a code system that uses an indented CodeCaretValueRule on a top-level concept', () => {
+    it('should parse a code system that uses an indented CaretValueRule on a top-level concept', () => {
       const input = `
       CodeSystem: ZOO
       * #anteater "Anteater"
@@ -586,17 +585,19 @@ describe('FSHImporter', () => {
       const codeSystem = result.codeSystems.get('ZOO');
       assertConceptRule(codeSystem.rules[0], 'anteater', 'Anteater', undefined, []);
       expect(codeSystem.rules[0].sourceInfo.file).toBe('Zoo.fsh');
-      assertCodeCaretRule(
+      assertCaretValueRule(
         codeSystem.rules[1],
-        ['anteater'],
+        '',
         'property[0].valueString',
-        'Their threat pose is really cute.'
+        'Their threat pose is really cute.',
+        false,
+        ['anteater']
       );
       expect(codeSystem.rules[1].sourceInfo.file).toBe('Zoo.fsh');
       expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
     });
 
-    it('should parse a code system that uses an indented CodeCaretValueRule on a nested concept', () => {
+    it('should parse a code system that uses an indented CaretValueRule on a nested concept', () => {
       const input = `
       CodeSystem: ZOO
       * #anteater "Anteater"
@@ -611,11 +612,13 @@ describe('FSHImporter', () => {
         'anteater'
       ]);
       expect(codeSystem.rules[1].sourceInfo.file).toBe('Zoo.fsh');
-      assertCodeCaretRule(
+      assertCaretValueRule(
         codeSystem.rules[2],
-        ['anteater', 'northern'],
+        '',
         'property[0].valueString',
-        'They are strong climbers.'
+        'They are strong climbers.',
+        false,
+        ['anteater', 'northern']
       );
       expect(codeSystem.rules[2].sourceInfo.file).toBe('Zoo.fsh');
       expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
@@ -639,18 +642,22 @@ describe('FSHImporter', () => {
         'anteater'
       ]);
       expect(codeSystem.rules[1].sourceInfo.file).toBe('Zoo.fsh');
-      assertCodeCaretRule(
+      assertCaretValueRule(
         codeSystem.rules[2],
-        ['anteater', 'northern'],
+        '',
         'property[0].valueString',
-        'They are strong climbers.'
+        'They are strong climbers.',
+        false,
+        ['anteater', 'northern']
       );
       expect(codeSystem.rules[2].sourceInfo.file).toBe('Zoo.fsh');
-      assertCodeCaretRule(
+      assertCaretValueRule(
         codeSystem.rules[3],
-        ['anteater'],
+        '',
         'property[0].valueString',
-        'Their threat pose is really cute.'
+        'Their threat pose is really cute.',
+        false,
+        ['anteater']
       );
       expect(codeSystem.rules[3].sourceInfo.file).toBe('Zoo.fsh');
       expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
@@ -678,14 +685,38 @@ describe('FSHImporter', () => {
         'anteater'
       ]);
       expect(codeSystem.rules[2].sourceInfo.file).toBe('Zoo.fsh');
-      assertCodeCaretRule(
+      assertCaretValueRule(
         codeSystem.rules[3],
-        ['anteater', 'northern'],
+        '',
         'property[0].valueString',
-        'They are strong climbers.'
+        'They are strong climbers.',
+        false,
+        ['anteater', 'northern']
       );
       expect(codeSystem.rules[3].sourceInfo.file).toBe('Zoo.fsh');
       expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
     });
+
+    it('should parse a rule set that uses an indented CaretValueRule on a top-level concept', () => {
+      const input = `
+      RuleSet: ZOO
+      * #anteater "Anteater"
+        * ^property[0].valueString = "Their threat pose is really cute."
+      `;
+      const result = importSingleText(input, 'Zoo.fsh');
+      const codeSystem = result.ruleSets.get('ZOO');
+      assertConceptRule(codeSystem.rules[0], 'anteater', 'Anteater', undefined, []);
+      expect(codeSystem.rules[0].sourceInfo.file).toBe('Zoo.fsh');
+      assertCaretValueRule(
+        codeSystem.rules[1],
+        'anteater',
+        'property[0].valueString',
+        'Their threat pose is really cute.',
+        false,
+        ['anteater']
+      );
+      expect(codeSystem.rules[1].sourceInfo.file).toBe('Zoo.fsh');
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+    });
   });
 });
diff --git a/test/testhelpers/asserts.ts b/test/testhelpers/asserts.ts
index 2480839..bdf3348 100644
--- a/test/testhelpers/asserts.ts
+++ b/test/testhelpers/asserts.ts
@@ -18,10 +18,10 @@ import {
   ValueSetConceptComponentRule,
   ValueSetFilterComponentRule,
   AddElementRule,
-  ConceptRule,
-  CodeCaretValueRule
+  ConceptRule
 } from '../../src/fshtypes/rules';
 import { FshCode, ValueSetFilter } from '../../src/fshtypes';
+import { splitOnPathPeriods } from '../../src/fhirtypes/common';
 
 export function assertCardRule(rule: Rule, path: string, min: number, max: number | string): void {
   expect(rule).toBeInstanceOf(CardRule);
@@ -105,7 +105,8 @@ export function assertCaretValueRule(
   path: string,
   caretPath: string,
   value: AssignmentValueType,
-  isInstance: boolean
+  isInstance: boolean,
+  pathArray?: string[]
 ): void {
   expect(rule).toBeInstanceOf(CaretValueRule);
   const caretValueRule = rule as CaretValueRule;
@@ -113,6 +114,7 @@ export function assertCaretValueRule(
   expect(caretValueRule.caretPath).toBe(caretPath);
   expect(caretValueRule.value).toEqual(value);
   expect(caretValueRule.isInstance).toBe(isInstance);
+  expect(caretValueRule.pathArray).toEqual(pathArray ?? splitOnPathPeriods(path).filter(p => p));
 }
 
 export function assertObeysRule(rule: Rule, path: string, invariant: string) {
@@ -270,18 +272,3 @@ export function assertConceptRule(
     expect(conceptRule.hierarchy).toEqual(hierarchy);
   }
 }
-
-export function assertCodeCaretRule(
-  rule: Rule,
-  codePath: string[],
-  caretPath: string,
-  value: AssignmentValueType,
-  isInstance = false
-) {
-  expect(rule).toBeInstanceOf(CodeCaretValueRule);
-  const codeCaretValueRule = rule as CodeCaretValueRule;
-  expect(codeCaretValueRule.codePath).toEqual(codePath);
-  expect(codeCaretValueRule.caretPath).toBe(caretPath);
-  expect(codeCaretValueRule.value).toEqual(value);
-  expect(codeCaretValueRule.isInstance).toBe(isInstance);
-}