# CI/CD Architecture — Acurity Validator in Azure DevOps

> **Audience:** Technology / Architecture leadership and senior engineers
> **Purpose:** Examine the current state of headless validation, the work required to
> make it pipeline-ready, how the codebase should be structured, and how to address
> security concerns around importing new tooling into a production Architect environment.

---

## 1 · Foundation — What `acurity.validateTxtFolder` Established

The command `acurity.validateTxtFolder` proved that the validation engine could
produce machine-readable output. When invoked from within VS Code it:

1. Asks the user to select a folder via a file-picker dialog
2. Discovers all `.txt` / `.TXT` files under that folder
3. Calls the same `collectDiagnostics()` engine used for real-time squiggle rendering
4. Serialises the results to a structured **JSON report** (file path, line, column,
   severity, rule ID, message)

### The coupling problem — now resolved

The command could not run outside VS Code because it depended on the VS Code host:

| Dependency | Where it appeared | Resolution |
|---|---|---|
| `vscode.workspace.openTextDocument()` | `batchValidateTxtFolder.js` | CLI reads files with `fs`; wraps content in `PlainTextDocument` |
| `vscode.workspace.getWorkspaceFolder()` | `diagnostics.js : collectDiagnostics()` | `workspaceRoot` is now an optional parameter; CLI passes it explicitly |
| `document.lineAt(n).text` / `document.lineCount` | throughout validators | `PlainTextDocument` satisfies this interface from a plain string |
| `vscode.Diagnostic`, `vscode.Range`, etc. | diagnosticFactory + all validators | `cli/vscode-shim.js` provides plain-object equivalents; intercepted via `Module._load` |

All blockers are resolved. The headless CLI is operational at `cli/index.js`.

---

## 2 · The Decoupling Work — Implemented

### 2a · Document adapter (`core/src/plainTextDocument.js`)

`collectDiagnostics` receives a document object. The interface it actually uses is narrow:

```
{ fileName, languageId, lineCount, uri: { fsPath }, lineAt(n): { text } }
```

`PlainTextDocument` satisfies this interface from a plain string:

```js
// core/src/plainTextDocument.js
class PlainTextDocument {
    constructor(filePath, content) {
        this._lines = content.split(/\r?\n/);
        this.fileName   = filePath;
        this.languageId = "acurity";   // satisfies isAcurityDocument()
        this.uri        = { fsPath: filePath };
        this.lineCount  = this._lines.length;
    }
    lineAt(n) { return { text: this._lines[n] ?? "" }; }
}
```

The VS Code extension continues to pass real `vscode.TextDocument` objects. No
changes to the validators were required.

### 2b · Parameterised workspace root (`vscode-extension/src/diagnostics.js`)

```js
// Before
function collectDiagnostics(document) { … }
const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath || null;

// After
function collectDiagnostics(document, options = {}) { … }
const workspaceRoot = options.workspaceRoot ?? vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath ?? null;
```

Existing VS Code callers are unchanged (the parameter defaults). The CLI passes
`workspaceRoot` explicitly.

### 2c · VS Code shim (`cli/vscode-shim.js`)

Because every validator and the diagnostic factory `require('vscode')` at load time,
the CLI intercepts `Module._load` before importing any extension code:

```js
// cli/index.js — first thing before any other require()
const Module = require("module");
const originalLoad = Module._load;
Module._load = function (request, parent, isMain) {
    if (request === "vscode") return require("./vscode-shim");
    return originalLoad.call(this, request, parent, isMain);
};
```

The shim provides plain-object implementations of `Diagnostic`, `Range`, `Position`,
`DiagnosticSeverity`, and a `workspace.getConfiguration` stub that returns catalog
default severities for all rules.

### 2d · CLI entry point (`cli/index.js`)

```
node cli/index.js --folder <path> [--output <file>] [--fail-on error|warning|none]
```

- Discovers all `.txt` / `.TXT` files recursively (skipping `node_modules`, `.git`)
- Wraps each in `PlainTextDocument`
- Calls `collectDiagnostics` with `workspaceRoot`
- Writes the same JSON report format as `acurity.validateTxtFolder`
- **Exits `1`** when any diagnostic meets or exceeds the `--fail-on` threshold — the
  mechanism Azure Pipelines uses to fail a build step

---

## 3 · Repository Structure

### Option A — Monorepo (implemented)

The source is already structured this way:

```
vs-code-acurity/                  ← one repo, three deployment targets
├── core/src/                     ← pure Node.js: parser, validators, registries
│   ├── analysis/                 │   Scope, function, macro analysis
│   ├── builtins/                 │   300+ built-in function registry
│   ├── diagnostics/              │   Factory, catalogue, quick-fix catalogue
│   ├── includeResolver/          │   Cross-file dependency graph
│   ├── validators/               │   15 independent rule engines
│   ├── parser.js                 │   Tokeniser and structural model
│   └── plainTextDocument.js      │   Headless document adapter
├── vscode-extension/src/         ← VS Code host integration (thin wrapper)
│   ├── commands/                 │   Batch validation, setup wizard
│   ├── config/                   │   Rule severity from VS Code settings
│   ├── diagnostics/              │   Scheduling and collection management
│   ├── diagnostics.js            │   collectDiagnostics() orchestrator
│   └── *Provider.js              │   Hover, completion, definition, actions
├── cli/                          ← Headless entry point
│   ├── index.js                  │   acurity-validate: scan → report → exit code
│   └── vscode-shim.js            │   Plain-object vscode type implementations
├── test/syntax-tests/            ← TDD fixture files (Architect code)
└── dev-docs/
```

**Advantages:** single PR covers validator change + test + CLI behaviour. The folder
separation enforces the architectural boundary without requiring separate npm packages
yet — that is Phase 2.
**Trade-off:** the Architect team's codebase remains in a separate repo.

### Option B — Two repos (stricter separation of concerns)

```
Repo 1: acurity-tooling            ← this repo (tooling authors)
Repo 2: acurity-architect-codebase ← the organisation's Architect source files
```

The pipeline in Repo 2 installs `@acurity/cli` from a private Azure Artifacts feed
and runs it against the checked-out source. The Architect developers never need to
touch Repo 1.

This is the **recommended model** for the organisation described — it cleanly separates
the people who write Architect code from the people who maintain the tooling.

### Option C — Inline CLI in the extension repo (minimal effort, lower maturity)

Keep everything in this repo; add a `cli/` folder at the top level. Publish a single
VSIX and a single tarball. Works well while the team is small; harder to govern as
the tooling grows.

---

## 4 · Azure DevOps Pipeline Integration

### 4a · Private npm feed (Azure Artifacts)

The compiled CLI package is published to an **Azure Artifacts feed** scoped to the
organisation. This avoids publishing to the public npm registry and gives the
organisation full control over which versions are available.

```yaml
# Pipeline in acurity-tooling repo — publishes the CLI on each release tag
- task: Npm@1
  inputs:
    command: publish
    workingDir: packages/cli
    publishRegistry: useFeed
    publishFeed: 'AcurityTooling'
```

### 4b · Validation pipeline in the Architect codebase repo

```yaml
# azure-pipelines.yml  (lives in the Architect codebase repo)
trigger:
  branches:
    include: ['*']

pool:
  vmImage: 'ubuntu-latest'

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: '20.x'

  - script: |
      npm install --global @acurity/cli \
        --registry https://pkgs.dev.azure.com/<org>/_packaging/AcurityTooling/npm/registry/
    displayName: Install Acurity validator

  - script: |
      acurity-validate \
        --folder $(Build.SourcesDirectory) \
        --output $(Build.ArtifactStagingDirectory)/acurity-report.json \
        --fail-on error
    displayName: Run Acurity validation

  - task: PublishBuildArtifacts@1
    inputs:
      pathToPublish: '$(Build.ArtifactStagingDirectory)/acurity-report.json'
      artifactName: 'acurity-validation-report'
    condition: always()        # publish report even when the step fails
```

**Exit code behaviour:** the `--fail-on error` flag causes the CLI to exit `1` if any
error-severity diagnostic is found, which causes Azure Pipelines to mark the step
(and therefore the build) as failed. Warnings can be reported without failing the
build by using `--fail-on none` or omitting the flag.

### 4c · Branch policies

In Azure DevOps, attach the pipeline to a **branch policy** on `main` / `master`:

> *Repos → Branches → Branch policies → Build validation*
> Add the Acurity validation pipeline as a required check.

This means no PR can complete without passing the validator — regardless of whether
the author has the VS Code extension installed.

---

## 5 · Security and Lockdown Considerations

This is the most operationally sensitive aspect of the proposal. The concerns fall
into two distinct categories.

### 5a · The VS Code extension — risk profile

VS Code extensions run with the **same OS-level privileges as the developer**. An
extension can:

- Read and write any file the user can access
- Spawn child processes
- Make outbound network calls

**Mitigations for the organisation:**

| Control | How to apply |
|---|---|
| **Restricted Extension Marketplace** | Azure AD + Intune can lock VS Code to an approved extension list, blocking installation of any extension not on the allowlist |
| **Private VSIX distribution** | Publish the extension as a `.vsix` file to an internal share or Azure Artifacts Universal Package — never via the public Marketplace — so only IT-provisioned installs are possible |
| **VS Code Extension Trust** | In VS Code 1.75+ the Restricted Mode workspace trust model can be enforced via policy; extensions that request elevated capabilities must be explicitly trusted |
| **Air-gapped / proxy environment** | If the Architect runtime environment has no outbound internet access, the extension is self-contained (no telemetry, no update checks against the Marketplace) |
| **Code review of the extension** | Because the source is internal, the team can audit every release before distribution — this is a significant advantage over trusting third-party Marketplace extensions |

### 5b · The CLI in CI/CD — risk profile

The CLI runs in an Azure Pipelines **agent** (typically a Microsoft-hosted or
self-hosted build agent), **not** on the production Architect runtime. The blast
radius is therefore contained:

- The agent has read access to the source repository checkout only
- The agent should run as a **low-privilege service account**, not a domain admin
- The agent machine should not have network access to production Architect servers
- The JSON report written by the CLI contains diagnostic metadata only — it never
  writes back to the source files

**Recommended agent policy:**

```
Agent permissions:
  - Read: source checkout directory
  - Write: $(Build.ArtifactStagingDirectory) only
  - Network: Azure Artifacts feed (npm install), Azure DevOps API (report upload)
  - No access to: production database, Architect runtime, file shares
```

### 5c · Dependency pinning

Both the extension and the CLI should pin their `@acurity/core` dependency to an
**exact version** (not a range). Combined with the private Azure Artifacts feed, this
ensures that:

- No new validator logic enters the pipeline without a deliberate version bump
- The exact code that was audited is the code that runs

```json
// package.json — use exact versions, not ranges
{
  "dependencies": {
    "@acurity/core": "1.4.2"
  }
}
```

### 5d · What the validator cannot do

It is worth being explicit about what the Acurity validator is architecturally
incapable of doing, to address fears about it "harming the production system":

- It **does not connect to any database or runtime** — it is a static analyser
- It **does not execute Architect code** — it reads and tokenises text files
- It **does not modify source files** — it produces a read-only JSON report
- It **does not have network access** unless the agent policy grants it

The closest analogy is ESLint for JavaScript: it inspects code, it does not run it.

---

## 6 · Phased Delivery Recommendation

| Phase | Scope | Outcome | Status |
|---|---|---|---|
| **1 — Decouple** | `PlainTextDocument` adapter; `workspaceRoot` param; `core/` · `vscode-extension/` · `cli/` folder split; vscode shim | Validators run headlessly with `node cli/index.js` | **Done** |
| **2 — Package** | Formal npm workspace setup; publish `@acurity/core` and `@acurity/cli` to Azure Artifacts | Installable from a pipeline without cloning this repo | Pending |
| **3 — Pipeline** | Add `azure-pipelines.yml` to the Architect codebase repo | Validation runs on every PR | Pending |
| **4 — Policy** | Attach pipeline as a required branch policy on `main` | Non-compliant code cannot merge | Pending |
| **5 — Governance** | Publish VSIX internally; apply Marketplace restriction policy | Developer installs are controlled | Pending |

Phase 1 is complete in this repo. Phases 2–5 require engagement with the
organisation's DevOps and IT teams.

---

## 7 · Summary

| Question | Answer |
|---|---|
| Does `validateTxtFolder` already solve CI/CD? | It proved the output format. The headless CLI (`cli/index.js`) now delivers the same capability without VS Code. |
| What engineering was required? | `PlainTextDocument` adapter, `workspaceRoot` parameter, vscode shim, CLI entry point, folder restructure — all done |
| Where does the code live? | Monorepo: `core/`, `vscode-extension/`, `cli/` — implemented |
| Where should the Architect codebase live? | In its own repo; it installs the CLI as a dev dependency from Azure Artifacts |
| Can the extension harm the production system? | No — it is a static analyser with no runtime or network connections |
| How do we control what gets installed? | Private VSIX + Marketplace restriction policy + pinned dependency versions |
