M16.5 adds explicit, project-scoped links between FieldFlow documents, drawings, and construction records. Relationships are created by authorized users; FieldFlow does not infer, extract, classify, or automatically create links.
One EntityRelationship model stores links across an explicit resolver
registry. This keeps the API and frontend workflow reusable without creating
another document system or a relationship table for every module.
The model stores:
id,project_id, andcreated_bysource_typeand positivesource_idtarget_typeand positivetarget_idrelationship_typecreated_at,updated_at, and nullabledeleted_at
The project and creator have native foreign keys. Polymorphic source and target IDs cannot have correct foreign keys to ten different tables, so integrity is enforced through an allowlisted resolver registry, project-owned lookups, availability checks, a controlled relationship matrix, and service transactions. This is a deliberate tradeoff; arbitrary model names, table lookups, and SQL identifiers are never accepted from clients.
Migration d9a2f5c8e173 follows c8f1a4d7e290 and creates only
entity_relationships. It adds checks for allowlisted types, positive IDs,
and distinct endpoints; project/source, project/target, relationship-type,
and created-order indexes; and a partial unique index for each active
canonical pair. Project deletion cascades relationship rows. Relationship
deletion itself is a soft delete.
The resolver registry supports exactly:
documentdrawing_setdrawing_sheetdrawing_revisiondrawing_issuerfisubmittalpunch_itemchange_orderdaily_log
Each resolver owns its model, safe identifier and title, status, route hint, search fields, deterministic ordering, availability, and new-link eligibility. Response summaries contain only those safe fields. They never contain storage keys, provider metadata, raw backend URLs, tokens, or model names.
Active Documents, active sheets, non-archived sets, and non-void issues may
be selected. Superseded drawing revisions remain selectable and navigable.
Archived drawing records remain factual in existing relationship lists but
cannot receive new links. A soft-deleted Document or hard-missing workflow
record is returned as Related record unavailable, without revealing
whether an inaccessible record exists in another project.
The controlled vocabulary and perspective labels are:
| Type | Forward label | Reverse label | Direction |
|---|---|---|---|
references |
References | Referenced by | Directional |
responds_to |
Responds to | Has response | Directional |
supersedes |
Supersedes | Superseded by | Directional |
supports |
Supports | Supported by | Directional |
impacts |
Impacts | Impacted by | Directional |
originated_from |
Originated from | Origin of | Directional |
resolves |
Resolves | Resolved by | Directional |
documents |
Documents | Documented by | Directional |
includes |
Includes | Included in | Directional |
associated_with |
Associated with | Associated with | Symmetric |
located_on |
Located on | Contains | Directional |
generated_by |
Generated by | Generated | Directional |
supersedes and generated_by are reserved vocabulary but have no generic
M16.5 matrix entries. Drawing revision lineage remains authoritative, and
Inspection support was intentionally deferred.
Only the following source, relationship, and target combinations can be created:
| Source | Relationship | Targets |
|---|---|---|
| RFI | References | Drawing Sheet, Drawing Revision |
| RFI | Responds to | Drawing Revision |
| RFI | Originated from | Submittal |
| RFI | Impacts | Change Order |
| Submittal | References | Drawing Sheet, Drawing Revision |
| Submittal | Responds to | RFI |
| Submittal | Impacts | Change Order |
| Punch Item | Located on | Drawing Sheet, Drawing Revision |
| Punch Item | Impacts | Change Order |
| Punch Item | Resolves | RFI |
| Change Order | Originated from | RFI, Submittal, Punch Item |
| Change Order | References | Drawing Revision |
| Change Order | Impacts | Drawing Sheet |
| Change Order | Resolves | RFI |
| Daily Log | Documents | RFI, Submittal, Punch Item, Change Order |
| Daily Log | References | Drawing Revision |
| Document | Supports | RFI, Submittal, Change Order |
| Drawing Issue | Includes | Document |
| Document | Associated with | Drawing Set, Drawing Sheet, Drawing Revision, Drawing Issue, RFI, Submittal, Punch Item, Change Order, Daily Log |
The UI exposes both perspectives of directional rows, but the database stores
one row. For example, an RFI can show that it is Origin of a Change Order
stored as Change Order originated_from RFI. associated_with endpoints are
sorted deterministically by type and ID before persistence, so selecting the
same pair in reverse cannot create another active row.
Generic links do not replace DrawingIssueRevision, drawing revision
supersession, Document version lineage, Attachment parent ownership, or
Folder hierarchy. Drawing Issue includes Document is supplementary context
and does not duplicate formal revision membership.
Every route requires authentication and get_owned_project:
GET /projects/{project_id}/relationshipsPOST /projects/{project_id}/relationshipsDELETE /projects/{project_id}/relationships/{relationship_id}GET /projects/{project_id}/relationship-candidates
The list route requires one entity_type and positive entity_id, supports
both, outgoing, incoming, or symmetric direction, and optionally
filters by relationship or related entity type. It defaults to 50 rows,
allows at most 100, uses a nonnegative offset, orders newest first with ID as
a stable tiebreaker, and returns total and has_more.
Candidate search accepts one allowlisted entity type, a normalized metadata query of at most 200 characters, and 1-50 results with a default of 20. Optional exclusion type and ID must be supplied together. Search is limited to safe identifiers and titles such as filenames, sheet numbers, revision codes, record numbers, dates, and summaries; it never searches file content or downloads binary data.
Create validates both endpoints within the owned project, the allowed
combination, availability, self-link rules, and active duplicates before one
commit. The server supplies project, creator, direction, labels, routes, and
timestamps. Unknown fields and lifecycle or route metadata are rejected.
Database uniqueness provides a final concurrency backstop, with duplicate
links returning 409. Failures roll back and do not update either related
entity or touch object storage.
Delete resolves only an active relationship in the owned project and marks
it deleted. A repeated or foreign delete receives the same safe 404.
Deleting a link never deletes a record, document, attachment, or specialized
domain association.
Relationship rows remain stored when a linked record is archived, soft-deleted, or hard-deleted. This preserves historical context and lets an authorized user remove a stale link. Existing archived drawing records and superseded revisions remain navigable where their domain routes permit it; unavailable entities lose their navigation route and cannot be selected for new links. A future permanent-purge lifecycle must explicitly account for polymorphic relationship rows.
useRelationships owns list, pagination, create, delete, retry, cancellation,
Strict Mode deduplication, and stale project/entity response rejection. The
reusable RelationshipPanel composes semantic relationship lists and the
CreateRelationshipDialog. It loads only when one persisted record context
is explicitly opened; create forms, table rows, and the dashboard do not
preload relationships.
The dialog derives relationship and entity choices from the same explicit matrix, performs debounced bounded candidate search, excludes the current record and active duplicates, rejects stale searches, and submits a server-valid directional payload. It uses labeled controls, a keyboard listbox, live search status, focus trapping and restoration, Escape handling, and pending/error states. Relationship items show label, entity type, identifier, title, status or availability, creation date, a descriptive navigation action, and a named remove action; color is never the only status signal.
Integrations use one active panel per page:
- Document details opens a page-level panel for the active Document.
- Drawing Register opens a panel for the selected Drawing Sheet.
- Drawing Viewer exposes Related Records in metadata for the exact current or historical Drawing Revision; opening or mutating it does not refetch the PDF.
- RFI, Submittal, Punch Item, Change Order, and Daily Log pages open a panel only for the selected persisted record.
Non-drawing links navigate to their existing project page. Drawing revisions navigate to the exact sheet/revision viewer route; drawing sets, sheets, and issues navigate to the existing Drawing Register. Unavailable summaries have no navigation action.
Source, target, list, candidate, and delete operations all inherit project
ownership. Two-user tests cover every resolver type, cross-project endpoints,
guessed IDs, foreign relationships, and candidate isolation. API responses
use Cache-Control: no-store for lists and candidates.
Relationship summaries are enriched in batches by entity type. The mixed
relationship integration fixture remains within 11 SQL SELECT statements,
including authentication and project resolution, rather than issuing a query
per row. Lists and searches are bounded; there is no full-project graph,
polling, table-row fan-out, dashboard request, attachment binary access, or
viewer PDF refetch.
M16.6 project document search remains a separate read-only capability. Extracted terms do not create, suggest, rank, or modify relationships, and the search route does not load the relationship graph. Users continue to create every link explicitly through the controlled candidate workflow.
Responsive CSS allows long labels, identifiers, and titles to wrap, stacks actions at narrow widths, bounds the dialog to the viewport, and keeps the viewer metadata integration usable. Automated semantic, keyboard, stale state, request-count, and page-integration coverage passes. Manual browser checks at 320, 375, 768, 1024, and wide desktop widths and at 200% zoom remain not verified in the current command-only environment.
The complete suites pass 720 primary tests: 433 frontend tests across 62 files and 287 backend tests, with 317 backend subtests reported separately. ESLint and the production build pass with no dependency changes. M16.5 adds a 17.96 kB raw / 5.45 kB gzip lazy relationship chunk. Relative to M16.4, main JavaScript changes by +1.23 kB raw / +0.27 kB gzip, the viewer by +0.93 kB raw / +0.23 kB gzip, and CSS by +5.02 kB raw / +0.64 kB gzip.
M16.7 reconfirms the same complete test totals and a final 17.97 kB raw /
5.45 kB gzip relationship chunk. The two-user API matrix, batched resolver
query test, lazy loading, stale-response, keyboard, and navigation tests pass;
live browser and responsive execution remains Not Verified and is listed in
DOCUMENT_QA.md.
The relationship system does not include automatic or AI-suggested links, document classification, graph visualization, bulk imports, comments, relationship version history, custom relationship types, or cross-project links. Document content search now exists independently, while production OCR remains unavailable. Inspection and delay-note resolvers were deferred to keep the initial matrix focused. Permanent-purge cleanup, drawing comparison, annotations, markups, and relationship analytics are also deferred; none is implied by the current explicit-link workflow.
M18.1 Review Sources and immutable run manifests are analysis inputs, not
construction-record relationships. They do not create, infer, or mutate
EntityRelationship rows. Existing human-created links remain authoritative,
and automatic relationship generation remains deferred.